静的解析と動的解析の原理(テイント解析・シンボリック実行)
なぜ SAST は実行せずに脆弱性を当て、なぜ誤検知が消えないのか。テイント解析のデータフロー、シンボリック実行の経路爆発、健全性と完全性のトレードオフを原理から理解し、ツールの限界を見抜けるようになる。
- 1.SAST(静的解析)はソースを実行せずに走査し、汚染源(source)から危険な出口(sink)へ未検証データが届く経路をデータフロー/テイント解析で追う。実行しないため全分岐を一度に見られる反面、実行時の値が不明で誤検知(false positive)が出やすい。
- 2.DAST/IAST(動的解析)は実際に動かして観測する。DAST は外から入力を送り応答で判定するため誤検知が少ない一方、踏んだ経路しか見ず見逃し(false negative)が増える。IAST は実行中のアプリ内部に計装を入れ、両者の中間を取る。
- 3.シンボリック実行は入力を具体値でなく記号として扱い、各分岐の条件式を経路制約として集め、SMT ソルバで充足解を求めて経路を体系的に探索する。だが分岐ごとに経路が分裂する経路爆発が本質的な壁になる。
静的と動的、何を見ているのか
プログラム解析には大きく二つの立場があります。静的解析(static analysis)はプログラムを実行せずにコードそのものを調べる手法で、セキュリティ文脈では SAST(Static Application Security Testing)と呼ばれます。動的解析(dynamic analysis)は実際に動かして振る舞いを観測する手法で、DAST(外部から)や IAST(内部に計装)がこれにあたります。
両者の根本的な違いは「見える情報」にあります。静的解析は全分岐・全経路を原理的に一度に俯瞰できますが、実行時にしか定まらない値(ユーザー入力、環境、外部応答)は分かりません。動的解析は実際の値と本当に起きた振る舞いを確実に掴めますが、その一回の実行で踏んだ経路しか見えません。この非対称性が、後述する誤検知と見逃しのトレードオフをすべて規定します。
扱うのは脆弱性検出のための解析エンジンの内部動作です。テイント解析のデータフロー追跡、シンボリック実行による経路探索、そして健全性(soundness)と完全性(completeness)のトレードオフを原理に絞って解説します。実際の動的検査(DAST/ファジング)は、許可された対象・環境でのみ行ってください。
テイント解析:汚染が出口へ届くかを追う
SAST の中核アルゴリズムが**テイント解析(taint analysis)**です。発想は単純で、「信頼できない入力(汚染源, source)が、危険な操作(出口, sink)へ、無害化(sanitizer)を通らずに届く経路はないか」を追います。届くなら脆弱性の疑いです。
- source:外部から来る信頼できないデータ。HTTP パラメータ、リクエストボディ、ヘッダ、ファイル、環境変数など。
- sink:汚染データが届くと危険な操作。SQL 文の組み立て、
eval、OS コマンド実行、HTML 出力、ファイルパス構築など。 - sanitizer:汚染を無害化する処理。プレースホルダによるパラメータ化、エスケープ、許可リスト検証など。
(source)req.query.id
│ 汚染が代入で伝播
▼
userId = req.query.id
│ 文字列連結で伝播
▼
sql = "SELECT * FROM users WHERE id = " + userId
│ sanitizer を通らずに到達
▼
(sink)db.query(sql) ← テイントが sink に届く=SQLi の疑い
解析器は変数や式に「汚染されているか」のラベルを付け、代入・連結・関数引数・戻り値を通じてこのラベルを伝播させます。源流が source、終点が sink、その間に sanitizer が無ければ警告を上げる――これが SAST がSQL インジェクションやXSSのような「未検証データの誤用」系を実行せずに見つけられる理屈です。
伝播の追跡には二つの土台があります。一つはデータフロー解析で、各プログラム点で「どの変数が汚染されうるか」という集合を、合流点で和を取りながら不動点(これ以上変化しない状態)まで反復計算します。もう一つは**制御フローグラフ(CFG)**で、文と分岐をノードとエッジで表し、解析の道筋を与えます。関数をまたぐ追跡(インタープロシージャル解析)では、呼び出しと戻りの対応を正しく取る必要があり、ここの精度がコストと誤検知を大きく左右します。
同じ変数名でも**呼び出し文脈(context)ごとに汚染状態は変わります。文脈を区別しない解析は、ある呼び出しの汚染を無関係な呼び出しへ漏らし、誤検知を量産します。同様に、行の順序を考慮する(flow-sensitive)か、a[i] のような配列・ポインタの別名(alias)**をどこまで精密に解くかで結果が変わります。精度を上げるほど計算量が跳ね上がるため、実装は常に精度とスケーラビリティの妥協点を選んでいます。
健全性と完全性:誤検知と見逃しの正体
解析の性質は二つの言葉で整理できます。**健全(sound)**とは「本物のバグを取りこぼさない(見逃しゼロ=偽陰性なし)」こと、完全(complete)とは「警告がすべて本物(誤検知ゼロ=偽陽性なし)」ことです。ライス(Rice)の定理が示すとおり、プログラムの非自明な意味的性質の判定は一般に決定不能であり、現実の解析器は両立を諦めてどちらかへ寄せます。
健全側に寄せた SAST は、判断に迷えば「危険かもしれない」と警告します。結果として見逃しは減りますが、実際には到達不能な経路や、実行時には決して成立しない条件まで警告し、誤検知が増えます。逆に完全側へ寄せれば警告は信用できますが、見逃しが増えます。SAST が誤検知だらけになりがちなのは、実行時の値を知らないまま「ありうる経路」を安全側に過大評価するからで、これは欠陥ではなく原理的な帰結です。
| 手法 | 見るもの | 強み | 弱み(主な誤差) |
|---|---|---|---|
| SAST(静的) | ソース/バイトコードの全経路 | 実行前に網羅的、行番号で指摘 | 実行時値が不明で誤検知が多い |
| DAST(動的・外部) | 稼働アプリの入出力 | 誤検知が少なく実証的 | 踏んだ経路のみ=見逃し、内部位置が不明 |
| IAST(動的・内部計装) | 実行中の内部データフロー | 実行経路に基づき低誤検知で位置も特定 | 計装が必要、未実行コードは見えない |
| シンボリック実行 | 記号入力での到達可能経路 | 経路ごとの具体的トリガ入力を生成 | 経路爆発でスケールしにくい |
DAST と IAST:動かして観測する
DAST は稼働中のアプリを外側のブラックボックスとして扱い、加工した入力(不正なパラメータ、攻撃ペイロード)を送り、応答や挙動の差分で脆弱性を推定します。実際に通信して反応を見るため、警告は実証的で誤検知が少ないのが利点です。一方、テストが到達できた経路しか評価できず、認証の奥や条件分岐の片側に潜む欠陥は見逃します。さらに「エラーが返った」事実は分かっても、コードのどこが原因かは外からは特定しにくいという弱みがあります。
IAST はその中間を取ります。アプリ内部に計装(instrumentation)を仕込み、テスト実行(手動でも DAST でも)の最中に実際に流れたデータフローを観測します。テイント解析を実行時に行うイメージで、source から sink への汚染伝播を本当に通った経路上で確認できます。実行経路に基づくため誤検知が低く、しかも脆弱な行を特定できます。ただし計装が前提で、一度も実行されなかったコードは見えないため、カバレッジの広さがそのまま検出力の上限になります。
DAST が「踏んだ経路しか見えない」という限界は、入力を能動的に生成して経路を広げるファジングの原理と表裏一体です。ファジングはカバレッジを報酬に深い経路へ自動到達し、IAST/DAST の見逃しを埋める動的探索の延長線上にあります。
シンボリック実行:入力を記号として解く
静的の網羅性と動的の具体性を橋渡しするのがシンボリック実行(symbolic execution)です。核心は、入力を 42 のような具体値ではなく、x という記号(シンボル)として扱う点にあります。プログラムを記号のまま「実行」すると、各変数は入力記号で表された式になり、分岐に出会うたびにその分岐を通る条件を集めていきます。
たとえば次のコードを考えます。
int f(int x) {
int y = x * 2;
if (y == 14)
if (x > 5)
crash(); // ここに到達できるか?
return 0;
}
シンボリック実行器は x を記号として始め、y = 2*x と式で保持します。if (y == 14) の真側へ進む経路では経路制約(path constraint)に 2*x == 14 を加え、内側の x > 5 の真側ではさらに x > 5 を加えます。crash() に至る経路の制約は 2*x == 14 ∧ x > 5 です(連言はインラインコードで 2*x == 14 AND x > 5 と読みます)。この制約を SMT ソルバ(Z3 など)に渡すと x = 7 という充足解が得られ、これがそのバグを実際に踏ませる具体的な入力になります。逆に制約が充足不能(unsat)なら、その経路は到達不能だと厳密に判定でき、SAST の誤検知の一因である「ありえない経路」を排除できます。
経路探索ツリー(各分岐で制約を分割)
start: x=記号
│
y==14 ? ┌───┴───┐
│真 │偽
2x==14 を追加 2x≠14(別経路)
│
x>5 ? ┌─┴─┐
│真 │偽
制約: 2x==14 ∧ x>5 2x==14 ∧ x≤5(到達せず)
→ SMT: x=7
→ crash() を踏む入力を生成
ここに動的記号実行(concolic, concrete + symbolic)という実用形があります。具体実行と記号実行を同時に走らせ、実際の値で一本の経路を進めながら、各分岐の記号制約を記録します。そして集めた制約のどれかを反転して SMT に解かせると、まだ踏んでいない隣の経路へ進む新しい入力が得られます。これを繰り返して経路を体系的に広げる――SAST の網羅志向と DAST の具体志向を、ソルバを介して結合した形です。
経路爆発:シンボリック実行の本質的な壁
シンボリック実行が万能でない理由は経路爆発(path explosion)にあります。分岐が一つあるたびに探索すべき経路はおおよそ二分裂し、独立した分岐が n 個あれば経路数は最悪 2^n に達します。ループは各反復が新たな分岐を生むため、回数が入力依存だと経路は事実上無限に膨らみます。すべての経路を解こうとすれば、時間もメモリも指数的に枯渇します。
分岐 0 個 : 1 経路
分岐 n 個 : 最悪 2^n 経路(独立分岐の場合)
→ n が 30 程度でも経路数は 10 億超
ループ k 回 : 反復ごとに分岐が積み上がり経路が乗算的に増殖
そこで実装は探索を間引きます。代表的な緩和策は次のとおりです。
- 経路の優先順位付け:未踏コードへ最短で届きそうな経路から探索する(カバレッジ誘導と同じ発想)。
- 状態のマージ:合流点で似た記号状態を一つにまとめ、分岐の積み上がりを抑える。
- 関数要約(summary):関数を毎回展開せず、入出力の制約関係に要約して再利用する。
- 具体化(concretization):ソルバが扱いにくい部分(複雑な暗号演算、外部呼び出し)を具体値で固定し、解ける範囲に問題を縮める。
これらは完全性を犠牲にしてスケーラビリティを買う取引です。間引いた経路に潜むバグは見逃しますが、現実的な時間で「踏める経路」を増やすために避けられません。ここでもまた、健全性・完全性・計算量の三つ巴がエンジン設計を支配しています。
シンボリック実行の検出力は SMT ソルバの解ける範囲にほぼ等しくなります。線形整数演算や配列・ビットベクトルは得意ですが、非線形演算(乗算が絡む式)や暗号ハッシュは原理的に苦手で、しばしばタイムアウトします。だからこそ「解けない部分は具体化する」緩和が要となり、解けるよう問題を整形するモデリングの巧拙が、そのまま到達できる経路の深さを決めます。
使い分け:層として組み合わせる
どの手法も単独では脆弱性を取りこぼします。実務では誤差の偏りが異なる手法を層として重ね、互いの死角を埋めます。
- SAST を早期・広範に:コミット単位で全経路を走査し、未検証データが sink へ届く疑いを実行前に拾う。誤検知前提でトリアージ運用を組み、安全側に倒れた警告を人手で仕分ける。
- DAST/IAST を稼働環境で:実際に動かして SAST の誤検知を裏取りし、見逃しを実証で補う。IAST を併用すれば実行経路上の汚染伝播を低誤検知で特定できる。
- シンボリック実行・ファジングを深部探索に:到達困難な経路や複雑な条件には記号実行で具体的トリガ入力を作り、ファジングで広く速く叩く。生成された入力は回帰テストの資産になる。
この層構造はOWASP Top 10が示すリスクを多面的に潰す現実解であり、SAST の誤検知も DAST の見逃しも、それぞれ別の手法で補完される前提で初めて意味を持ちます。
(1) SAST は実行せずソースを走査し、テイント解析で source→sink への未検証データ伝播を追う。実行時値が不明なため誤検知(偽陽性)に偏る。(2) DAST は外部から動かして観測し誤検知が少ないが、踏んだ経路しか見ず見逃し(偽陰性)に偏る。IAST は内部計装で両者の中間を取り、実行経路上の汚染を低誤検知で位置特定する。(3) 健全(sound)=見逃しなし/完全(complete)=誤検知なしで、決定不能性ゆえ両立は不可能。(4) シンボリック実行は入力を記号化し、分岐ごとに経路制約を集めて SMT ソルバで充足解(=トリガ入力)を求める。concolic は具体実行と併走し制約反転で新経路を開く。(5) 本質的な壁は**経路爆発(分岐 n で最悪 2^n 経路)**で、状態マージ・要約・具体化で完全性と引き換えにスケールさせる。
まとめ:何を見ていないかを知る
プログラム解析の要点は、各手法が**「何を見て、何を見ていないか」**を正しく押さえることに尽きます。SAST は全経路を見るが実行時の真値を見ないので誤検知に、DAST は真の振る舞いを見るが踏んだ経路しか見ないので見逃しに、それぞれ原理的に偏ります。シンボリック実行はその溝を制約ソルバで橋渡ししますが、経路爆発という指数の壁が立ちはだかります。
これらはツールの未熟さではなく、決定不能性と計算量に根ざした避けられないトレードオフです。だからこそ、偏りの異なる手法を層として組み合わせ、誤検知はトリアージで仕分け、見逃しは動的探索で埋める――この設計思想こそが、メモリ破壊攻撃のような深い欠陥を含め、人手のレビューでは届かない問題を継続的に削り取る現実的な道筋になります。
セキュリティ Article
静的解析と動的解析の原理(テイント解析・シンボリック実行)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
静的解析
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 5
導入後に効く点
DAST/IAST(動的解析)は実際に動かして観測する。DAST は外から入力を送り応答で判定するため誤検知が少ない一方、踏んだ経路しか見ず見逃し(false negative)が増える。IAST は実行中のアプリ内部に計装を入れ、両者の中間を取る。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「静的解析 / テイント解析」に近いか確認する。
- 強みである「SAST(静的解析)はソースを実行せずに走査し、汚染源(source)から危険な出口(sink)へ未検証データが届く経路をデータフロー/テイント解析で追う。実行しないため全分岐を一度に見られる反面、実行時の値が不明で誤検知(false positive)が出やすい。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。