自分の書いたコードを検査する ── 静的解析

上原正吉(EarthLink Network Co., Ltd.)。AI会社・AI社員による業務自動化と、人の逐次介入なしに24時間自動開発を回す仕組みを自社で実践しています。開発ノウハウと具体的な設定方法、運用して分かった改善点を公開しています。

自分の書いたコードを検査する ── 静的解析

自分の書いたコードを検査する ── 静的解析

この記事は、連載「自分のサービスは自分で守る ── AI 時代の自衛セキュリティ」の第 6 回です。自分でできる検査は自分で、難しいときは EarthLink Network が検査を代行し、直せない弱点は私たちが修正します。

連載の目次

  1. AIで誰でも攻撃できる時代 ── やられる前に、自分のサイトを自分で検査する
  2. 攻撃する側が持っているツール ── AI が自分で攻撃を進めるとは
  3. 検査を始める前に ── 検査の全体像と、本番を壊さない線引き
  4. 外から攻めて検査する① ── 準備と、最初の実行まで
  5. 外から攻めて検査する② ── 攻撃ツールと正規ツールで、見つかるものは違う
  6. 自分の書いたコードを検査する ── 静的解析(この記事)
  7. 使っている部品の弱点を検査する ── 依存ライブラリ(予定)
  8. インフラの設定の誤りを検査する(予定)
  9. 検査の結果を 1 か所に集めて、どれから直すかを決める(予定)
  10. 見つかった弱点の直し方 ── 3 つの道(予定)
  11. 自分で直せないときは ── 保守契約という選択(予定)
  12. 毎日自動で検査し続ける ── 作る時代から、守り続ける時代へ(予定)

結論

前回までの 2 回は、動いているサービスに外から問い合わせを送る検査と、コードを読む AI のツール(Strix)の検査を比べました。今回は、コードを読む検査そのもののやり方です。自分たちが書いたソースコードを読んで、書き方に弱点が無いかを調べます。この検査を静的解析と呼びます。

自分のサービスなら、ソースコードは手元にあります。だから、外から試すより先に、まずコードを調べるのがいちばん安くて速いやり方です。無料のツールで、全部のファイルを同じ基準で、数分で調べられます。

ただし、調べ方で、見つかる弱点が変わります。私たちは無料の Semgrep(セムグレップ)で、自社サービス promptflow のコードを調べました。

  • セキュリティ向けのルールセットでは、0 件でした。
  • JavaScript 向けのルールセットに替えると、1 件見つかりました。 ログインのあとに、どの画面へ移動するかを決めている 1 行です。
  • その 1 行の確かめ方の見落としを見つけたのは、AI のコードレビュー(CodeRabbit)でした。 Semgrep のルールには、その書き方が入っていません。(すでに直してあります)

つまり、一つの調べ方で 0 件でも、安全とは限りません。

もう一つ、APIキー(サービスを使うための合言葉の文字列)やパスワードを、コードに書き残していないかも調べました。無料の Gitleaks(ギットリークス)で Git の履歴まで探すと、過去に書き込まれた AWS(アマゾンのクラウド)のアクセスキーが残っていました。そのキーはもう無効で、使えない状態でした。

この記事は、次の三つの大きな流れで進めます。

  1. 調べる。 静的解析の 3 通りの調べ方と、promptflow で試した結果。秘密の書き残しの探し方も、ここに入ります。
  2. 誤検知かどうかを確かめる。 見つかったものが本当に危ないのかを、どこで・何を見て見分けるか。
  3. 定期的に調べ直す。 一度の 0 件で終わらせない仕組み。

1. 調べる

静的解析とは

静的解析は、ソースコードのファイルを一つずつ読んで、「この書き方は危ない」という場所を探す検査です。見つかると、その場所(ファイル名と行番号)と、なぜ危ないかの説明を出してくれます。

外から試す検査と比べると、得意なことと苦手なことがはっきり分かれます。

  • 全部のファイルを、同じ基準で調べられる。 外から試す検査は、外から届く入口しか調べられません。コードを読む検査なら、まだ画面に出していない機能や、管理者だけが使う機能のコードも、同じように調べます。
  • 安くて速い。 無料のツールが多く、数千のファイルでも数分で終わります。
  • 動かしたときにだけ分かることは、見えない。 たとえば、サーバーの応答にセキュリティのためのヘッダ(応答に付ける設定)が付いているかどうかは、コードを読むだけでは分かりません。これは第 4 回・第 5 回の、外から試す検査の役目です。

どちらか一つで足りる、ということはありません。今回は、コードを読む側を見ていきます。

調べ方は 3 通りある

静的解析のツールは、弱点の見つけ方で、大きく 3 通りに分かれます。見つけ方が違えば、見つかる弱点も違います。

  • ルールに照らす。 「この書き方は危ない」というルールを何百個も用意しておき、全部のファイルと照らし合わせます。ルールの集まりを、ルールセットと呼びます。決まった危ない書き方を、一気に探すのが得意です。逆に、ルールに無い書き方は見つけられません。無料で使えるものに、Semgrep(Community Edition)と、SonarQube(ソナーキューブ)の Community Build があります。
  • 値の流れを追う。 外から届いた値が、どのファイルを通って、どこで使われるかを追います。たとえば「利用者が書き換えられる値が、確かめられないまま、データベースへの命令に入っている」といった弱点を見つけます。ファイルをまたいで追えるので、ルールに照らすだけの調べ方より、見当違いの指摘が減ります。無料で使えるのは、Semgrep の無料プラン(登録して使う版)と、公開しているリポジトリでの GitHub の CodeQL(コードキューエル)です。SonarQube もこの調べ方を持っていますが、有料のプランからです(SonarQube Cloud なら Team 以上)。
  • AI が読む。 AI がコードを読んで、人がレビューするように指摘します。ルールに無い書き方や、「この確かめ方では足りない」といった処理の意味の誤りに気づけます。第 4・5 回で使った Strix も、この種類のツールです。私たちは、ふだんのレビューには CodeRabbit(コードラビット)を使っています。プルリクエスト(書いた変更を、本流のコードに入れる前に確認してもらう依頼)に、CodeRabbit が自動でコードを読んで指摘を書き込みます。手元のコマンドから動かして、書きかけの変更を見てもらうこともできます。

どれも無料から始められます。くわしい無料の範囲は、記事の後半にまとめました。

同じコードを、ルールセットを替えて 2 回調べる

ここからは、promptflow のコードで試した結果です。調べたのは、この記事で扱う弱点を直す前のコードのコピーです。本番のサービスには触れていません。

Semgrep は Python の部品として配られているので、python3 -m pip install semgrep の 1 行で入ります。まず、アプリのコードのフォルダを、セキュリティ向けのルールセット(p/security-audit)で調べました。

Semgrep で、promptflow のアプリのコードをセキュリティ向けのルールセットで調べた画面。実行したコマンドは「semgrep scan --config p/security-audit --metrics=off --json --output …/semgrep-security-audit-src.json src/」。下の Scan Summary に「Findings: 0 (0 blocking)」「Rules run: 23」「Targets scanned: 2016」と出て、その下に「Ran 23 rules on 2016 files: 0 findings.」の行がある。出力先のフォルダは「…/」に縮めた。

2,016 ファイルを、約 2 分で調べ終わりました。結果は 0 件です。

次に、同じコードを JavaScript 向けのルールセット(p/javascript)で調べました。

同じコードを JavaScript 向けのルールセットで調べた画面。コマンドは「semgrep scan --config p/javascript --metrics=off --json --output …/semgrep-javascript-src.json src/」。「Findings: 1 (1 blocking)」「Ran 74 rules on 1730 files: 1 finding.」。下は、結果のファイルから jq(JSON から必要な部分を取り出すツール)で取り出した 1 件の中身。check_id は javascript.browser.security.open-redirect.js-open-redirect、場所は src/app/auth/callback/page.tsx の 53 行目、重さは WARNING。説明文は、利用者が操作できる値で画面の移動先が決まると、別のサイトへ飛ばされたり、悪いプログラムを動かされたりするおそれがある、という内容。

今度は 1 件見つかりました。

見つかったのは、ログインのあとに、どの画面へ移動するかを決めている 1 行です。Semgrep の説明を平たく言うと、こうなります。「移動先を、利用者が書き換えられるかもしれない値で決めている。もし外からその値を書き換えられたら、利用者を偽のサイトへ飛ばせてしまう。その値を使う前に、確かめてほしい」。この弱点は、オープンリダイレクトと呼ばれます。偽のログイン画面へ誘い込む手口に使われます。

この 1 件のルールは、セキュリティ向けのルールセットには入っていませんでした。同じ調べ方(ルールに照らす)でも、ルールセットを替えるだけで、見つかるものが変わります。

調べ方を替えると、ルールに無い書き方が見つかる

では、この 1 行は危ないのか。コードを追うと、移動先の値は、使う前に別のファイルの関数で確かめられていました。移動先を同じサイトの中の場所(/settings のような書き方)に限り、https:// で始まる別のサイトや、// で始まる書き方は、トップページに置き換える関数です。

ところが、この確かめ方には見落としがありました。// は弾くのに、/\(スラッシュの次に逆スラッシュ)で始まる値は通していたのです。ブラウザは、URL の中の逆スラッシュを、スラッシュと同じに扱うことがあります。だから /\ は // と同じように、別のサイトへの移動として読まれることがあります。細工したリンクを踏ませてログインまでしてもらうと、ログインのあとに別のサイトへ飛ばせてしまう書き方でした。

この見落としを見つけたのは、Semgrep ではありません。この 1 行を直している途中で、CodeRabbit をコマンドから動かしてレビューさせたところ、/\ を指摘しました。今回使った Semgrep のルールセットには、この書き方が入っていませんでした。ルールに照らす調べ方と、AI が読む調べ方では、見つけるものが違う、という例です。この指摘を受けて、/\ で始まる値もトップページに置き換えるように直しました。

どうやってコードを追ったかの細かい手順は、記事の最後の補足に載せました。

0 件でも、安全とは限らない

最初のルールセットの 0 件は、「安全」という意味ではありませんでした。0 件が意味するのは、「このルールセットのルールに合う書き方は無かった」ということだけです。理由は三つあります。

  • ルールの数が限られている。 使われたルールは 23 個でした。23 個で、世の中のすべての危ない書き方を拾えるわけではありません。
  • 無料版は、ファイルをまたいで追わない。 Semgrep の公式の説明では、登録せずに使う無料版(Community Edition)は、一つの処理のまとまり(関数)の中しか調べません。値がどのファイルから来たかは追いません。
  • 途中であきらめたファイルがある。 結果のファイルの中を見ると、22 のファイルで「時間切れ」の警告が記録されていました。画面の表には「Scan completed successfully(正常に終了)」と出るので、結果のファイルまで見ないと気づきません。

だから、調べ方を替えたり、重ねたりします。ルールセットを足す。値の流れを追うツールを使う。AI のレビューを入れる。一つで 0 件でも、別の調べ方で出てくることがあります。

秘密の書き残しを、Git の履歴まで探す

静的解析には、もう一つ仕事があります。APIキーやパスワードなどの秘密を、コードにそのまま書き残していないかを探すことです。

これは、ここまでの話とは危なさの種類が違います。コードの書き方の弱点は、今動いているサイトが外から破られるかどうかの話でした。秘密の書き残しは、サイトの応答には出てきません。外から何万回問い合わせても見つかりませんし、それだけで外から破られることもありません。

困るのは、リポジトリ(コードと変更の履歴をまとめて置いておく場所)そのものが、人の手に渡ったときです。たとえば、GitHub のアカウントや、開発者のパソコンが破られたときです。そのとき、コードに書き残したキーがあれば、そのキーでクラウドを使われたり、データを抜かれたりします。

しかも、Git(コードの変更を一つずつ記録する仕組み)は、過去のすべての変更を残しています。キーを書き込んだあと、次の変更でファイルから消しても、消す前の記録は残ります。消したつもりでも、リポジトリを手に入れた人は、過去の記録から取り出せます。だから、今のファイルだけでなく、Git の履歴まで探します。

Gitleaks は GitHub の配布ページから、自分のパソコンに合った実行ファイルを取ってくるだけで使えます。promptflow のリポジトリで、Git の履歴をすべて調べました。

Gitleaks で、promptflow のリポジトリの過去の変更をすべて調べた画面。コマンドは「gitleaks git . --report-format json --report-path …/gitleaks-git.json --redact」。出力に「7633 commits scanned.」「scanned ~330617031 bytes (330.62 MB) in 1m26s」「WRN leaks found: 2」。下は、結果のファイルから jq で取り出した 2 件。1 件目は RuleID が aws-access-token、2 件目は generic-api-key。どちらも File は scripts/local/compare-rag-e2e.mts で、行は 165 と 166。Secret は REDACTED と伏せてある。

過去の変更 7,633 件を、1 分 26 秒で調べ終わりました。見つかったのは 2 件です。どちらも同じファイルの、隣り合った 2 行でした。1 件目は AWS のアクセスキーの ID の形、2 件目はそれと組になる秘密の文字列の形をしていました。

コマンドに付けた --redact は、必ず付けてください。付けないと、見つかったキーの値が、画面と結果のファイルにそのまま書き出されます。結果のファイルを誰かに渡した時点で、キーを二か所目に漏らすことになります。

2. 誤検知かどうかを確かめる

ルールに照らすツールは、「念のため」で指してきます。指されたものが全部危ないわけではありません。だから、一件ずつ誤検知かどうかを確かめます。

コードの書き方の指摘を見分ける

見る場所は、指された行と、その値の出どころです。値がどこから来て、使う前に確かめているかを見ます。

値の出どころを追った結果誤検知かどうか次にすること
値は外からは来ない(プログラムの中で決めた値だけ)誤検知理由を書いて、誤検知の印を付ける
外から来るが、使う前に確かめている誤検知(ツールには、確かめている処理が見えていない)確かめている場所を書いて、誤検知の印を付ける
確かめているが、確かめ方が足りない弱点(軽い)確かめ方を直す
外から来て、どこでも確かめていない弱点優先して直す

今回の Semgrep の 1 件は、最初は表の 2 行目に見えましたが、追っていくと 3 行目でした。ツールの結果だけでは、どの行に当たるかは分かりません。分かるのは、人か AI がコードを追ったときです。ここが、ツールに任せきれないところです。

秘密の書き残しを見分ける

秘密の書き残しなら、見るのは三つです。説明書の見本の値ではないか。テスト用に作った値ではないか。今も使えるか。

Gitleaks は値を伏せて出すので、指された変更の中身を手元で表示して確かめました(値は、この記事にも作業の記録にも書き出していません)。

  • 見本でもテスト用でもなかった。 AWS のアクセスキーの ID は、決まった 4 文字で始まる 20 文字の英数字です。指された行は、その形をしていました。説明書によくある見本のキー(EXAMPLE で終わるもの)でもありませんでした。手元で試すための小さなプログラムに、キーが直接書き込まれていました。
  • ファイルからは消してあったが、Git の履歴には残っていた。 今のファイルには、このキーはありません。それでも、Git の履歴からは取り出せました。ファイルから消しても、履歴には残る、の例です。
  • キーは、もう使えなかった。 AWS に、読み取りだけの問い合わせをして確かめました。このキーは「無効(Inactive)」になっていて、同じ利用者には新しいキーが作られていました。古いキーは止めて、新しいキーに取り替えてあった、ということです。

見つかったときに、最初に確かめるのは「そのキーが、今も使えるか」です。キーの止め方と、Git の履歴からちゃんと消すやり方は、第 10 回(直し方)で扱います。

誤検知に印を付けて、次から出ないようにする

誤検知だと分かったら、印を付けて、次の検査から出ないようにします。そうしないと、毎回同じものが出続けて、新しく出た弱点が埋もれます。

  • Semgrep: 指された行に // nosemgrep: <ルールの名前> と書き、その横に誤検知だと考えた理由を書きます。
  • Gitleaks: テスト用の値なら、その行に gitleaks:allow と書くか、見つかったものの指紋(見つかったもの一つずつに付く、識別のための文字列)を .gitleaksignore というファイルに書いて外します。

大事なのは、印と一緒に理由を書くことです。理由の無い印は、弱点をごまかすのにも使えてしまいます。外す範囲も、一行ずつ狭くします。テスト用のファイルを丸ごと外すような広い外し方をすると、そこにうっかり書いたキーも見逃すからです。

3. 定期的に調べ直す

一度調べて 0 件でも、それは「その日の、その調べ方で 0 件」というだけです。

  • コードは毎日変わる。 新しく書いたコードには、新しい弱点が入ることがあります。
  • ルールは増える。 新しい攻撃の手口が知られると、それを見つけるルールが足されます。昨日は見つからなかった弱点が、今日のルールで見つかることがあります。
  • 秘密の書き残しは、いつでも起きる。 手元で試すつもりで書いたキーを、うっかりそのまま取り込むことは、誰にでも起きます。

だから、静的解析は一度で終わらせず、仕組みにして繰り返します。私たちは、同じ検査を次の 3 つのときに実行しています。

  • プルリクエストのとき。 Semgrep を JavaScript・Python・セキュリティ向けの三つのルールセットで実行し、Gitleaks と、部品の弱点を調べる Trivy(第 7 回で扱います)も実行します。CodeRabbit もコードを読みます。新しく増えた重い指摘があれば、取り込みを止めます。
  • 取り込んだ直後。 Semgrep・Gitleaks・Trivy を、もう一度実行します。
  • 毎日。 GitHub に付いている自動実行の仕組み(GitHub Actions)で、Semgrep・Gitleaks・Trivy を実行します。

人が毎朝何かをしているわけではありません。一度設定すれば、機械が毎日調べます。結果は、第 9 回で扱う「結果を集める場所」に送り、毎日の実行の画面は第 12 回でお見せします。

無料でできる範囲

今回のツールは、すべて無料から始められます。料金は、各社の公式の料金ページで確かめました。

ツール調べ方無料で使える範囲
Semgrep(Community Edition)ルールに照らす制限なし(オープンソース)
Semgrep(無料プラン)ルールに照らす・値の流れを追うリポジトリ 10 個・10 人まで
SonarQube(Community Build)ルールに照らす自分のサーバーで動かせば制限なし。値の流れを追う検出は有料版から
SonarQube Cloud(Free プラン)ルールに照らす(値の流れを追う検出は Team 以上)非公開のコードは 5 万行まで
GitHub CodeQL値の流れを追う公開しているリポジトリだけ。非公開は GitHub の Team 以上のプランに加えて、1 人あたり月 30 ドル
CodeRabbitAI が読む公開しているリポジトリは、プルリクエストのレビューまで無料。非公開は、プルリクエストの要約と、エディタ・コマンドからのレビュー(1 人あたり 1 時間に 3 回まで)
Gitleaks秘密の書き残しを探す制限なし(オープンソース)

私たちのリポジトリは非公開なので、CodeRabbit はプルリクエストのレビューまで使える契約にしています。CodeQL と、GitHub の秘密の書き残しの検査(非公開は 1 人あたり月 19 ドル)は使っていません。代わりに、無料の Semgrep と Gitleaks を毎日実行しています。くわしい料金の比べ方は、第 3 回に書きました。

次回

次回、第 7 回は、使っている部品の弱点を検査します。自分たちが書いたコードは、サービス全体のごく一部です。残りの大半は、外から取ってきた部品(ライブラリ)でできています。その部品に知られた弱点が残っていないかを調べる検査を、同じようにコマンドと画面で進めます。件数がいちばん多く出るのは、この検査です。

補足

Semgrep の 1 件を、コードで追った手順

指された 53 行目と、その値がどこから来るかを並べました。

Semgrep が指した行と、その値の出どころのコード。上は src/app/auth/callback/page.tsx の 41〜55 行目で、53 行目「window.location.href = result.returnTo;」が強調されている。下は別のファイル shared/src/services/auth.ts の 392〜397 行目(sanitizeReturnTo という関数。先頭が「/」でない値と、「//」で始まる値を「/」に置き換える)と、549〜567 行目(resolveSilentSsoCallbackError という関数。557 行目で、ブラウザに保存しておいた値と届いた値が一致しなければ何もしない。565 行目で、移動先を sanitizeReturnTo に通してから返す)。

  1. 53 行目は、result.returnTo(ログインのあとの移動先)へ画面を移しています。Semgrep が指したのはここです。
  2. この値は、別のファイルにある関数(resolveSilentSsoCallbackError)が作っています。
  3. その関数は 557 行目で、URL に付いてきた state(ログインの前後で同じブラウザかを確かめる値)を、このブラウザ自身がログインを始めるときに保存しておいた値と比べています。一致しなければ、何もせずに終わります。外から好きな値を付けて送り込んでも、ここで止まります。
  4. さらに 565 行目で、移動先を sanitizeReturnTo という関数に通しています。この関数は、移動先を同じサイトの中の場所(/settings のような書き方)に限り、https:// で始まる値や // で始まる値をトップページ(/)に置き換えます。

確かめる処理が別のファイルにあるので、ファイルをまたいで追わない無料版の Semgrep には、この処理が見えません。

ただし、557 行目の照合が止めるのは、外から state をすり替えることだけです。移動先は、ログインの入口の画面でも決められます。

  1. ログインの入口の画面(/sso)は、URL に付けた returnTo の値を、ログイン後の移動先として受け取ります。
  2. その値は sanitizeReturnTo を通ります。/\ で始まる値は、ここを素通りします。
  3. 素通りした値を、利用者のブラウザ自身が state に入れて保存します。
  4. ログインが終わって戻ってくると、state は自分で保存したものなので一致します。そのまま、/\ で始まる移動先へ画面が移ります。

この最後の画面の移動は、Semgrep が指した 53 行目ではなく、同じファイルの 33 行目で起きます。Semgrep が指したのは、別のサイトへ移動しうる 2 か所のうち、1 か所だけでした。

コマンドの意味

  • semgrep scan --config <ルールセット>:どのルールセットで調べるかを指定します。p/security-audit や p/javascript は、Semgrep の公開の置き場(レジストリ)から自動で取ってきます。
  • --metrics=off:使い方の統計を、Semgrep の会社に送らない指定です。
  • --json --output <ファイル名>:結果を、あとで機械で扱える形式(JSON)でファイルに保存します。時間切れの警告のように、画面に出ない情報もここに残ります。
  • gitleaks git .:今いるリポジトリの、Git の履歴を全部調べます。今のファイルだけを調べるときは gitleaks dir を使います。
  • --redact:見つかった値そのものを、画面にも結果のファイルにも出さず、REDACTED と伏せます。

用語

  • オープンソース:作り方が公開されていて、誰でも無料で使えるソフトです。
  • リポジトリ:コードと、その変更の履歴をまとめて置いておく場所です。
  • Git:コードの変更を一つずつ記録する仕組みです。GitHub は、Git のリポジトリを置いておくサービスです。
  • プルリクエスト:書いた変更を、本流のコードに入れる前に確認してもらう依頼です。ここでレビューや自動の検査が行われます。
  • ルールセット:「この書き方は危ない」というルールを、目的ごとに集めたものです。

この連載では、攻撃ツールが誰でも無料で手に入る時代に、自分のサービスを自分で検査し、弱いところを先に見つけて直す手順を、順に示します。検査の対象は自社サービスに限り、他人のサイトを攻撃する手順は扱いません。 自分で検査するのが難しい方は、EarthLink Network が検査を代行します。弱点は見つかったが自分では直せない、という方は、私たちが修正まで引き受けます。お気軽にご相談ください。 役に立ったと感じたら、ぜひ「いいね」と記事の購読をお願いいたします。

自社プロダクトは https://www.eln.ne.jp/products にまとめています。

筆者について

上原正吉。EarthLink Network Co., Ltd. 代表。AI社員に日々の業務を任せ、開発では人が逐一指示しなくても作業が進むHOOTL(Human Out Of The Loop)の仕組みを自社で実践しています。このブログでは、業務自動化と24時間自動開発のノウハウ、具体的な設定方法、日々の運用で見つかった問題と改善を共有します。

また、AIで業務や開発を組み替えたい会社・チーム向けに、AI活用のコンサルティングも受け付けています。ご相談は www.eln.ne.jp からどうぞ。


EarthLink Network は、会社の全業務を AI で回すために、必要になったものを自社で作っています。いま作っているプロダクトの一覧と概要は、こちらにまとめています。

→ EarthLink Network が自社でつくっている18のプロダクト

会社と各プロダクトの詳細は、公式サイト www.eln.ne.jp をご覧ください。