SQLインジェクションの脆弱性があるJavaScriptファイルに対してESLintを実行すると、問題なく通過します。同じコードベースに対してCodeQLを実行すると、正確なファイル、行、データフローパスとともにインジェクションが検出されます。どちらも静的解析ツールです。どちらも実行せずに同じソースコードを検査します。検出結果の違いは、ルール設定や言語サポートの問題ではなく、各ツールがどこまで調べるかという根本的な違いです。ESLintは一度に1つの関数を調べ、その範囲内で見つかったものを報告します。CodeQLは、関数呼び出し、モジュール間、そして完全な呼び出しグラフを通してデータを追跡し、危険なものに到達するまで追跡します。
プロシージャ内分析とプロシージャ間分析という区別は、異なる静的解析ツールが異なる結果をもたらす理由を理解する上で最も重要な技術的概念です。リンターがコードベースに問題がないと判断する一方で、より高度なツールが同じコード内に重大なセキュリティ脆弱性を発見する理由、セキュリティSASTツールの実行コストが高い理由、そして「高速」ツールの偽陰性率が「低速」ツールの偽陰性率よりも構造的に高い理由を説明しています。これは、企業チームが静的解析ツールチェーンを評価する前に理解しておくべき概念です。
術中解析とは何ですか?
手続き内解析は、単一の関数、メソッド、またはプロシージャの内部で何が起こるかを検証するもので、その関数が他の関数を呼び出す場合や、他の関数から呼び出される場合は考慮しません。解析の境界は関数の境界です。手続き内解析ツールは関数呼び出しに到達するとそこで停止し、呼び出し先まで追跡せず、呼び出し先が受け取ったデータに対して何を行うかも知りません。
手続き内分析の典型的な例としては、リンティングと単一関数内の基本的なデータフローが挙げられる。
パイソン
def process_input(user_data):
# Intraprocedural analysis sees: user_data used here
query = "SELECT * FROM users WHERE id = " + user_data # flagged: string concat in SQL
return query
SQLクエリへの文字列連結を検出する単純なルールは、危険なパターンが単一の関数内で確認できるため、手続き内では有効です。しかし、コード構造を少し変更してみましょう。
パイソン
def get_user_id(request):
return request.args.get("id") # untrusted input
def build_query(user_id):
return "SELECT * FROM users WHERE id = " + user_id # looks safe in isolation
def handle_request(request):
uid = get_user_id(request)
query = build_query(uid) # vulnerability spans three functions
db.execute(query)
汚染されたデータのソース(request.args.get("id"))は危険なシンクとは異なる機能を持っています(db.execute(query)) 単一の関数には、手順内ツールが脆弱性を検出するのに十分な情報が含まれていない。 get_user_id 見た目は問題ない。リクエストから読み込んでいるだけだ。 build_query 問題なさそうだ。パラメータを受け取って文字列を構築する。 handle_request 見た目は問題なく、関数を呼び出し、クエリを実行します。SQLインジェクションは実際に存在し、悪用可能であり、関数境界を越えないツールからは検出できません。
術中分析の強みとは?
プロシージャ内ツールは高速です。非常に高速です。各関数を独立して解析するため、コードベースのサイズに比例して処理速度が上がり、100万行のコードベースの解析コストは1万行のコードベースの解析コストとほぼ同じ割合になります。この速度により、以下のような用途に最適です。
- コミット前のフック 1秒未満で走る
- IDEインラインフィードバック 開発者が入力している間
- CI/CD品質ゲート すべてのプルリクエストで
- スタイルと慣習の遵守 (命名規則、書式設定、複雑性指標)
- ローカル変数の誤用 (未使用変数、未定義参照、明らかな型エラー)
- 循環的複雑度とメソッドの長さ純粋に部門内指標
このカテゴリに含まれるツールには、ESLint、Pylint、Clippy(Rust)、golangci-lint、Checkstyle、およびほとんどのリンターが含まれます。これらは、検出できる問題の種類に応じて、不可欠かつ価値のあるツールです。これらのツールの限界は構造的なものであり、品質上の欠陥ではありません。
インタープロシージャル解析とは何ですか?
プロシージャ間(プログラム全体)解析は、一般的に最も強力で、最も多くのリソースを消費します。プロシージャ間解析では、制御フローグラフに呼び出しグラフからの関数呼び出しと戻りエッジを追加します。関数サマリーを作成することで、同じ関数を何度も参照する必要がなくなります。
プロシージャ内解析は関数境界で停止するのに対し、プロシージャ間解析は境界を越えます。プロシージャ間解析は、どの関数がどの関数を呼び出すかを示す呼び出しグラフを作成し、そのグラフのエッジに沿ってデータフロー、制御フロー、または汚染を追跡します。プロシージャ間解析ツールが関数呼び出しに遭遇すると、呼び出し先まで呼び出しを追跡するか、呼び出し先の動作に関する事前計算された要約を使用して、反対側で解析を継続します。
上記の3つのSQLインジェクション関数は単純な例です。実際のプロシージャ間解析では、以下の処理が行われます。
- マルチホップデータフロー: 危険なシンクに到達するまでに 5 つの関数を通過する信頼できない入力
- 戻り値の汚染関数が汚染されたデータを返す。呼び出し元はその汚染された戻り値を安全でない操作で使用する。
- 場に敏感な汚染オブジェクトフィールドに格納された信頼できないデータは、後で別の関数で取得されます。
- 条件付き伝播: 汚染は、分析が静的にモデル化する必要のある実行時条件に応じて、特定の実行パスに沿ってのみ流れます。
ジャワ
// Interprocedural vulnerability: three-hop taint flow
public class UserController {
public String handleRequest(HttpServletRequest request) {
String userId = request.getParameter("id"); // source: untrusted input
User user = userService.getUser(userId); // taint flows through call
return generateReport(user); // taint reaches sink
}
}
public class UserService {
public User getUser(String id) {
// id is tainted -- passed to repository
return userRepository.findByRawId(id); // taint propagates
}
}
public class UserRepository {
public User findByRawId(String id) {
String query = "SELECT * FROM users WHERE id = " + id; // sink: SQL injection
return db.execute(query);
}
}
術中検査ツール UserRepository.findByRawId 単独ではパラメータが id SQLクエリに連結されます。これは潜在的に危険に見えますが、ツールにはどこが危険かを知る方法がありません。 id 由来は、内部コードからの信頼できる検証済みの値である可能性があります。そのため、プロシージャ内ツールは、それを潜在的な問題としてフラグ付けするか(コードベース内のすべてのパラメータ化された構造化クエリで誤検出を生成)、無視するか(実際の脆弱性で誤検出を生成)します。プロシージャ間汚染分析は、信頼できないデータが関数を介して機密性の高い操作に流れているかどうかを検出します。 request.getParameter("id") userService.getUser()を通じて、 userRepository.findByRawId()へ db.execute(query)正確な汚染経路を示す精密な結果が得られ、偽陽性も偽陰性もありません。
分析の3つのレベル:実践的なスペクトル
実際の静的解析ツールは、純粋にプロシージャ内処理のみを行うものから、完全にプロシージャ間処理を行うものまで、幅広い範囲に分布しています。それぞれのツールがどの位置づけにあるかを理解することで、検出できるものと見逃してしまうものの両方を把握できます。
| 分析レベル | 対象領域 | 見ているもの | 何が欠けているか | ツール |
|---|---|---|---|---|
| 語彙的/統語的 | 個々のトークンと構文 | 書式設定、命名規則、基本構文 | 意味論的なすべて | より美しく、rustfmt、gofmt |
| 術中 | 単一機能 | 関数内のローカルデータフロー、複雑性、制御フロー | 機能横断的な脆弱性、アーキテクチャ上の問題 | ESLint、Pylint、Clippy、golangci-lint |
| 浅い手術間 | 呼び出し深度は1段階です。 | 直接の呼び出し元と呼び出し先の関係 | ディープマルチホップフロー | Semgrepのルール、基本的なSASTルール |
| 完全なインタープロセス | 完全なコールグラフ走査 | マルチホップ汚染フロー、コンテキスト依存分析 | 実行時多態性(部分的)、リフレクティブ呼び出し | CodeQL、Facebook Infer、 SMART TS XL |
| プログラム全体/意味論 | 完全なプログラム意味論 | 形式的に証明可能な性質 | 非常に大規模なコードベースにおけるスケーラビリティ | Infer、Kani(Rust)、形式検証ツール |
プロシージャ間解析は、潜在的有効性は高いものの、潜在的パフォーマンスは比較的低いという特徴があります。豊富なプログラムモデルのおかげで、偽陰性や偽陽性の発生が少なく、比較的高い有効性を実現しています。しかし、パフォーマンス上の制約から開発者の作業速度を低下させる可能性があるため、プロシージャ間解析はCI/CD環境には最適とは言えません。そのため、プロシージャ間解析は、サーバーファームや専用ツール上で動作する専用サービスとして提供されることが多いです。
手続き間分析の仕組み:3つの主要なアプローチ
1. 要約に基づく分析
分析では、関数サマリー(モデルと呼ばれることもあります)を作成することで、同じ関数を何度も再検証することを回避できます。サマリーは、関数の動作を簡潔な形式で記述します。つまり、この汚染特性を持つ一連の入力が与えられた場合、関数はこの汚染特性を持つ一連の出力を生成します。呼び出し元が関数を呼び出すと、分析は関数本体を最初から再分析するのではなく、サマリーをインスタンス化します。
要約ベースの分析こそが、プロシージャ間分析を大規模に実行可能にする鍵です。要約がない場合、ライブラリを呼び出す関数を分析するには、ライブラリが呼び出すすべての関数を含め、ライブラリ全体をソースから再帰的に分析する必要があります。要約があれば、ライブラリ関数は一度事前分析され、その要約は呼び出しごとに再利用されます。
2. 呼び出し文字列アプローチ(コンテキスト依存分析)
コンテキスト依存分析は、同じ関数の異なる呼び出し箇所を区別します。 sanitize(input) 信頼できるデータと信頼できないデータの両方を含む呼び出しが行われる場合、コンテキストに応じた分析では、すべての呼び出し箇所に対して最悪のケースを保守的に想定するのではなく、呼び出し箇所ごとに異なる結果が得られます。これにより、コンテキストを考慮しない分析と比較して、誤検出が大幅に減少します。
IFDS(Interprocedural, Finite, Distributive, Subset)フレームワークは、これをグラフ到達可能性問題として定式化します。IFDSフレームワークでは、汚染の流れは有限の事実領域に対する分配関数によって記述され、手続き間伝播はグラフ到達可能性に還元されます。この定式化により、IFDS分析は正確かつアルゴリズム的に効率的になり、FlowDroid(Android用)やいくつかのCodeQLクエリなどのツールは内部的にIFDSを使用しています。
3. コールグラフの構築
手続き間解析には必ず呼び出しグラフが必要です。呼び出しグラフとは、ノードが関数、エッジが呼び出しを表す有向グラフです。呼び出しグラフ自体を構築するには、次のような難しい判断が伴います。
静的ディスパッチ (Javaの非仮想呼び出し、Cの関数呼び出し、Pythonのモジュールレベルの呼び出し): モデル化が容易、 f() 呼び出し f.
動的ディスパッチ(Javaの仮想メソッド、Pythonのダックタイピング呼び出し):特定の呼び出し箇所で実際に呼び出される可能性のあるメソッドの実装を決定するために、クラス階層分析(CHA)またはより精密な呼び出しグラフ構築アルゴリズム(RTA、VTA)が必要です。
リフレクション呼び出しと関数ポインタ:最も難しいケースです。ツールは、過剰近似(一致する関数はすべて呼び出される可能性があると想定し、誤検出を生む)するか、過小近似(一部の呼び出しパスを見逃し、誤検出を生む)するかのどちらかを選択する必要があります。
コールグラフの精度は、手続き間解析の精度に直接影響します。エッジが多すぎるコールグラフ(過剰近似)は偽陽性を生成し、エッジが欠落しているコールグラフは偽陰性を生成します。
手術間分析では、手術内分析ではできないことがわかっています。
セキュリティ脆弱性(汚染分析)
OWASP Top 10の脆弱性の大部分は、確実に検出するために手続き間汚染分析を必要とします。
- SQLインジェクション信頼できない入力が複数の関数を経由してデータベースクエリに渡される
- XSS信頼できない HTML がレンダリング関数を介して出力コンテキストに流れます
- コマンドインジェクション: ユーザーが制御するデータが到達する
exec()orsystem()中間機能を介して - パストラバーサルユーザー入力から派生したファイルパスはファイルシステム操作に到達します
- SSRF: ユーザーが制御する URL が HTTP クライアント呼び出しに渡される
パイソン
# Intraprocedural tool: sees no issue in any individual function
# Interprocedural tool: traces the full taint path and reports SSRF
def get_target_url(request):
return request.json().get("url") # source: attacker-controlled
def fetch_resource(url):
return requests.get(url) # sink: SSRF
def api_endpoint(request):
target = get_target_url(request)
data = fetch_resource(target) # full path: source -> sink
return data
ヌルポインタとメモリ安全性の問題
Facebook Inferは、ヌル安全性解析のための標準的な手続き間ツールです。手続き間推論の一種である双アブダクションを用いて、関数の事前条件と事後条件を検出し、呼び出し元がそれらの事前条件を満たしているかどうかを確認します。特定の条件下で関数がヌルを返す可能性があることを知る必要があるヌル逆参照は、手続き内ツールでは検出できません。
ジャワ
// Intraprocedural tool: no issue in either function
// Infer: finds that processResult() can receive null from fetchData()
String fetchData(boolean condition) {
if (condition) return getData();
return null; // can return null
}
void processResult(String result) {
System.out.println(result.length()); // NPE if result is null
}
void run() {
String data = fetchData(false); // passes false: fetchData returns null
processResult(data); // null dereference
}
解放後使用とリソースリーク
C言語およびC++言語では、解放後使用(use-after-free)バグは、ある関数で割り当てられたメモリが別の関数で解放され、さらに別の関数が参照を保持している場合に発生することがよくあります。単一関数解析では、この種のエラーを検出することはできません。プロシージャ間解析では、制御フローの整合性を分析し、関数呼び出しが想定されるパターンに従っていることを確認することで、悪用を防止します。
アーキテクチャ上の結合と依存関係の違反
セキュリティと正確性を超えて、プロシージャ間分析はアーキテクチャ分析にも適用されます。どのコンポーネントがどのコンポーネントを呼び出すか、どの依存関係が定義されたレイヤー境界に違反するか、どの変更が呼び出しグラフ全体に連鎖するかなどです。これは、次のようなツールが使用するプロシージャ間分析の応用です。 SMART TS XL エンタープライズコードベースに適用し、ある関数への変更が呼び出しグラフ全体にどのように伝播するかを追跡して、真の影響範囲を判断します。
手術間分析が必要な場合
すべてのプロジェクトで、完全なプロシージャ間解析が必要なわけではありません。その判断は、何を見つけようとしているのか、そしてそれを見逃した場合の損失はどれくらいなのかによって決まります。
以下のような場合に、処置間分析が必要になります。
- セキュリティは最優先事項であり、インジェクション、XSS、SSRFの脆弱性を確実に発見する必要があります。
- あなたは、多くの関数やモジュールにまたがるビジネスロジックを持つレガシーコードベースまたはエンタープライズコードベースを分析しています。
- リファクタリングを行う前に影響分析を行い、関数の変更によって何が影響を受けるかを理解する必要があります。
- あなたは、徹底性を実証する必要のあるコンプライアンスまたは監査分析を実施しています。
- コードベースを移行または近代化する際に、コンポーネント間の依存関係を理解する必要があります。
術中分析は、以下の場合には十分である:
- 目標は、スタイルの徹底、慣習の遵守、そして明らかなローカルな間違いの発見である。
- 開発ループに迅速なフィードバックを組み込む必要があります(コミット前、IDE、プルリクエストごとのCI)。
- コードベースは十分に小さいため、開発者はすべての呼び出し関係を把握できます。
- セキュリティは別のレイヤー(検証済みフレームワーク、型システム)で実施されており、機能間の脆弱性のリスクは低い。
ほとんどのチームにとって現実的な解決策は、両方を使うことです。プロシージャ内ツール(ESLint、Pylint、Clippyなど)はコミットごとに実行され、迅速なフィードバックが得られます。プロシージャ間ツール(CodeQL、Infer、汚染ルール付きSemgrepなど)はプルリクエスト時、または毎晩実行されるスケジュールで実行され、徹底的な分析が行われます。この2つのレイヤーは競合するものではなく、相互補完的な関係にあります。
手順間解析ツール:それぞれの対象範囲
| ツール | 分析アプローチ | 言語 | 主なユースケース |
|---|---|---|---|
| コードQL | 完全なインタープロセス、IFDSベース | C/C++、Java、JS/TS、Python、Go、Ruby | セキュリティ脆弱性検出 |
| Facebook 推論 | 両外転、手術間 | Java、C、C++、Objective-C | 安全性の欠如、メモリリーク、競合状態 |
| Semgrep(汚染モード) | 浅い処置間汚染 | 30以上の言語 | データフローを用いたセキュリティパターンマッチング |
| スニックコード | MLベースのインタープロシージャ | Java、JS/TS、Python、C# | 開発者にとって使いやすいフィードバックを備えたセキュリティ |
| ポリスペース | 抽象的解釈 | C、C++、Ada | 安全性が極めて重要な組み込みシステム |
| フラマC | 完全なインタープロセス+正式な | C | 形式検証、安全認証 |
| SMART TS XL | 言語間、システム間 | COBOL、JCL、Java、Python、RPG、SQL | 企業影響分析、依存関係マッピング |
言語境界を越えた手続き間分析
標準的なプロシージャ間ツールは、単一の言語内で動作します。Javaコードベースに対するCodeQLクエリは、Java関数呼び出しを通じて汚染を追跡しますが、JavaコードがCOBOLプログラムを呼び出したり、JCLバッチジョブによって書き込まれたデータセットを読み取ったりすると停止します。この境界こそが、エンタープライズ規模の分析ツールが直面する問題であり、単一言語ツールでは対処できません。
SMART TS XLさん 静的コード分析 エンタープライズ環境のすべての言語にわたって、同時に手続き間スタイルの依存関係分析を実行します。Java サービスが、別の JCL ジョブによってデータが投入される DB2 テーブルからデータを読み取る COBOL プログラムを呼び出す場合、 SMART TS XL その連鎖をたどり、それを統一された依存関係モデルとして表現します。 影響分析 この機能では、この言語間呼び出しグラフを適用して、この COBOL プログラムが変更された場合、どの Java サービス、どの JCL ジョブ、およびどの下流のコンシューマーが影響を受けるかという問いに答えます。
これは、機能レベルではなくアーキテクチャレベルで適用される手続き間問題であり、個々のプログラムではなくエンタープライズシステム全体に適用される同じ原則(境界で止まるのではなく、境界を越えて依存関係グラフをたどる)です。コードの可視化と依存関係マッピングの文脈で検討されているように、完全な依存関係構造を可視化することは、変更が何に影響を与えるかを理解しようとするあらゆる分析の前提条件となります。
組織が実施する場合 レガシーの近代化コールグラフは SMART TS XL 言語をまたいで構築されるものは、単一言語内の手続き間呼び出しグラフと同じ目的を果たします。つまり、すべてがどのように接続されているかを示す地図であり、これがないと、影響、リスク、または移行シーケンスの分析は、証拠ではなく仮定に基づくものになってしまいます。
偽陽性と偽陰性のトレードオフ
プロシージャ間解析が広く普及していない理由は、誰も思いつかなかったからではなく、精度とパフォーマンスが相反するからです。100万行のコードベースに対して、コンテキストを考慮した完全なプロシージャ間解析を行うには、数時間から数日かかる場合があります。一方、ESLintで同じコードベースを解析すると、わずか数秒で済みます。このパフォーマンスの差は構造的なものであり、プロシージャ間解析はコードベースのサイズだけでなく、呼び出しグラフの複雑さにも比例して増大します。
この投資のメリットは、偽陽性率と偽陰性率が著しく低下することです。
- 偽陽性 (実際には問題がないのに、問題があると誤検出するツール):プロシージャ内ツールは、呼び出し元からのコンテキストがわからないと、疑わしいパターンが実際に危険かどうかを判断できないため、誤検出がはるかに多くなります。連結された文字列を含むSQLクエリを検出したプロシージャ内ツールは、その文字列がユーザー入力によるものか、信頼できる内部ソースによるものかを判断することができません。
- 偽陰性 (ツールが実際の脆弱性を見逃す):手続き内ツールは、機能境界をまたぐ脆弱性を検出できないため、偽陰性が多くなります。セキュリティ関連の分析においては、これは実際の脆弱性の大部分を占めます。
各分析レベルをいつ使用するかを選択し、それらを階層化されたツールチェーンで組み合わせる規律こそが、セキュリティエンジニアリングを単なるチェックリストへの準拠と区別するものです。リンターがクリーンな結果を出したとしても、セキュリティ体制についてはほとんど何も分かりません。一方、プロシージャ間汚染分析が、外部入力ソースからアプリケーション内の機密性の高い処理箇所までのすべてのパスを追跡し、検証されていないフローが見つからなかったと分かれば、はるかに多くのことが分かります。


