sql-security について
ホスト言語のコードで組み立てるSQLを確認するためのセキュリティスキルです。Node.jsスキャナーが一部の単一行補間や文字列連結を検出し、検出範囲の限界と認証情報チェック用のsecretlintも説明します。
主な機能
- 01SQL補間パターンのソーススキャン
- 02SQL文字列連結の検出
- 03生成物とテストファイルの除外
- 04確認済み検出への除外コメント
- 05認証情報検出にsecretlintを利用
活用シーン
- 01変更をpushする前のSQLスキャン
- 02PRレビューへのスキャン結果の追加
- 03誤検出の可能性がある項目の手動確認
- 04リポジトリ内のハードコード認証情報の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mizchi/ skills --skill sql-securityインストール後、Claude Code で/sql-securityを入力して読み込まれたか確認します。
同梱ファイルREADME.mdscripts/sql-injection-scan.mjs
詳細
SQL セキュリティ
ホストコード(MoonBit/TS/Rust/Go)から SQL を実行するプロジェクトで、SQL 関連インシデントの主な原因となる次の 2 つに対して、手軽な防御策を導入したい場合に使います。
- SQL インジェクション:プレースホルダーを通じてバインドする代わりに、文字列テンプレートで値を SQL フラグメントに埋め込むことです。
- クエリ内のシークレット:ハードコードされたトークンや接続文字列が git の履歴に漏れることです。これはこのスキルではなく
secretlintで対処します。詳しくは以下の「併用ツール:secretlint」を参照してください。
sql-injection-scan.mjs
node scripts/sql-injection-scan.mjs your-project/src
このスキャナーはディレクトリを走査し、生成ファイル(db/gen/、sqlc_*.mbt、*.test.mbt、_build/、dist/、target/)を無視して、次のものを検出します。
- template-interp:SQL キーワード(
SELECT、INSERT INTO、WHERE、AND (など)で始まり、${...}プレースホルダーを含むバッククォート文字列リテラル。例:`WHERE c.vector_id IN (${placeholders})`。 - string-concat:引用符で囲まれた SQL 文字列の直後に
+と識別子が続くもの。例:"SELECT * FROM " + table。
キーワードの照合では、意図的に大文字と小文字を区別しています。from/where/join は英語の文章で頻繁に使われるため、大文字と小文字を区別せずに照合すると、誤検出が何百件も発生するためです。
検出結果がある場合、スキャナーは終了コード 1 を返します。安全だと判明した場合でも、すべての検出結果を手動で確認してください。
制限事項 — スキャン結果がクリーンでも安全性の証明にはなりません
スキャナーは1 行ずつ読み取ります。テンプレートリテラルのルールが適用されるには、開始バッククォート、${...} プレースホルダー、終了バッククォートがすべて同じ物理行にある必要があります。これにより、次のような結果になります。
-
複数行のテンプレートリテラルは、検出されずに見逃されます。 バッククォートがある行で始まり、
${value}が継続行にあるクエリ(よくある整形スタイル)は、実際にはインジェクションであっても、検出件数 0、終了コード 0 になります。スキャナーが検出できない例:const sql = ` SELECT id FROM users WHERE name = ${name} // <-- real injection, not flagged `; -
終了コード 0 の意味は「1 行で完結する検出対象が見つからなかった」であり、「安全」ではありません。 SQL インジェクションをレビューする際は、複数行の SQL やプログラムで組み立てられる SQL も手動で確認してください。スキャン結果がクリーンだからといって、レビューを通過したと判断してはいけません。スキャナーに検出させたい場合は、複数行のクエリを 1 行にまとめてください。
これは意図的なコストと網羅性のトレードオフ(依存関係ゼロ、パーサーなし)であり、バグではありません。ただし、レビュー時に補う必要がある見落としの可能性があります。
検出結果の読み方
検出結果があっても、必ずしもインジェクションとは限りません。正当なケースも多くあります。
- 内部でのプレースホルダー展開:サーバー由来の件数から、IN 句用の
?,?,?,...,?を組み立てるケース。埋め込まれる値はユーザー入力ではなく?です。今後のスキャンで無視できるよう、オプトアウトコメントを付けてください。 - sqlc が解析できない FTS5/ベクトルクエリ:カラムリストや
?の数を埋め込む動的な句ビルダー。ユーザー入力は引き続き.bind(...)を通ります。 - インラインのマイグレーションスクリプト:デプロイ時に DDL を組み立てます。
実際の SQLi を 1 件見逃すコストは、誤検出を 5 件トリアージするコストよりはるかに高いため、スキャナーは意図的に多めに検出します。
オプトアウトマーカー
安全だと確認済みの行を検出対象から外すには、同じ行または直前の行に sql-security: ok マーカーを含むコメントを追加してください。
// sql-security: ok (placeholders is server-derived `?` count, values bind separately)
const sql = `WHERE c.vector_id IN (${placeholders})`;
スキャナーは //(TS/MoonBit/Rust/Go)と #(Python/シェル)のコメントマーカーを受け付けます。
併用ツール:secretlint
認証情報の漏えい(「SQL セキュリティ」のもう半分)には secretlint を使います。pkfire で管理するリポジトリの pre-push フックには、次の設定をおすすめします。
pnpm add -D secretlint @secretlint/secretlint-rule-preset-recommend
続いて、pre-push で次を実行します。
pnpm exec secretlint --secretlintignore .gitignore "**/*"
これはリポジトリごとの設定であり、ユーザー全体に適用される secretlint の設定を補完します。pkfire フックを使うプロジェクト向けに、mizchi/skills/pkfire スキルには assets/recipes/14-secretlint-pre-push.pkl にすぐ使えるレシピがあります。
実行するタイミング
- pre-push:軽量で、コードをリモートに送信する前に 1 回実行します。
- pre-commit:任意です。ここでの
secretlintは大きな差分に対して遅くなる場合がありますが、sql-injection-scanは十分軽量です。 - PR レビュー:スキャナーの出力をレビュー用チェックリストに貼り付けます。
対象外
- 保存型 XSS/テンプレートインジェクション:これらは SQL ではなく、フロントエンド/テンプレートのレイヤーを経由します。
- 認可のバイパス:行レベルのアクセスチェックはアプリケーションのロジックであり、SQL の問題ではありません。
- 負荷の高いクエリによる DoS:
sql-plan-audit(新しい SCAN エントリを探します)とエッジでのレート制限で対応します。
エンジンの拡張性
スキャナーの正規表現はエンジンに依存しません。SQL キーワードのセットは、SQLite/Postgres/MySQL ですぐに利用できます。Postgres を多用するプロジェクトでより幅広くカバーする必要がある場合は、RETURNING/OVERLAPS/LIMIT OFFSET を追加してください。
要件
- Node 20 以降。
ファイル
scripts/sql-injection-scan.mjs— 依存関係のないテキストスキャナー。