owasp-audit について
owasp-auditは、OWASP Top 10:2025に沿ってソースコードを体系的に確認するスキルです。許可されたコード監査を対象とし、相談、全体監査、変更範囲のレビューを区別します。候補の検証とカバレッジ記録も行います。
主な機能
- 01OWASP Top 10:2025に沿った監査手順
- 02サーフェスと信頼境界のカバレッジ記録
- 03候補指摘の独立検証
- 04JSONの指摘・カバレッジ検証
- 05監査記録からのレポート生成
活用シーン
- 01許可されたコードベースのセキュリティ確認
- 02マージ前のPRやコミット範囲の監査
- 03再実行時のカバレッジと判定の追跡
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ briiirussell/ cybersecurity-skills --skill owasp-auditインストール後、Claude Code で/owasp-auditを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/checklist-details.mdcoverage.schema.jsondiff-review.mdfindings-format.mdfindings.schema.jsonorchestration.mdrecon-and-coverage.mdseverity-calibration.mdto-sarif.mjsvalidate-coverage.mjsvalidate-findings.mjsverification.mdwebhooks-and-parsers.md詳細
翻訳後に原文が更新されています。最新の内容は原文をご確認ください。
OWASP 監査 — ソースコードのセキュリティレビュー
アプリケーションのソースコードを OWASP Top 10(2021)に照らして体系的にセキュリティ監査します。
監査範囲の特定
- プロジェクトの言語、フレームワーク、アーキテクチャを特定する
- エントリーポイント(ルート、API ハンドラー、フォーム処理機能)を洗い出す
- データフロー(ユーザー入力 → 処理 → 保存 → 出力)を特定する
- 認証と認可の境界を特定する
監査チェックリスト
各カテゴリーを体系的に確認します。既知の脆弱性パターンを grep で検索し、該当したファイルを読み込んで詳しく分析します。
A01:アクセス制御の不備
- エンドポイントやルートの認可チェックの欠落
- IDOR — 所有者の検証がないユーザー指定 ID
- 認証チェックの順序。 リソースの存在、状態、その他のリソース固有のメタデータが明らかになる可能性のある分岐より前に、認可チェックが実行されることを確認します。「見つからない」場合に 404、「状態が不正」な場合に 400、「未認証」の場合に 401 を返すこと自体が情報漏えいになります。攻撃者は認証ゲートを通過することなく、リソース ID を列挙して状態を把握できてしまいます。推奨される応答形式:権限のない呼び出し元に表示すべきでないものには、すべて一律に 404 を返します。
- ルートとしては見えないフレームワークの RPC サーフェス。 Server Actions などは、ファイルのスキャンでは見つからない、外部に公開された RPC です。それぞれを列挙し、認証と所有権を監査します。
- Next.js:
'use server'を含むファイル内の、エクスポートされたすべての関数 - Remix / React Router:エクスポートされたすべての
action/loader - tRPC:すべてのプロシージャ
- GraphQL:すべてのリゾルバー
- Rails:リソースに対応しないコントローラーアクション
- Next.js:
- ミューテーションのペイロードに含まれる外部キーを介した IDOR。 フォームから外部キー UUID(
categoryId、projectId、teamId、organizationId)が送信される → サーバーは主レコードの所有権を検証するが、外部キーをそのまま受け入れる → 後続の ORM のリレーション結合で別のテナントのデータが表示される。formData.get("<id>")/body.<id>が、先行するfindFirst({ where: { id, userId } })なしで、そのまま insert/update に渡されていないか確認します。ORM のリレーション結合(Drizzle のwith:、Prisma のinclude、ActiveRecord のincludes)については、結合先が親クエリと同じテナント/所有権の述語でフィルタリングされているか追跡します。 - 状態を変更するリクエストに対する CSRF 保護の欠落
- ロールチェックがフロントエンドにしかなく、サーバー側で強制されていない
- 認証後の戻り先パラメーターを介したオープンリダイレクト —
?from=、?next=、?returnTo=、?continue=、?redirect=がサニタイズされずにredirect()/Response.redirect()に渡されているケース。同一オリジンのパスに限定し、想定されるスコープ内に収めます。トラバーサル(/admin/../fooなど)を防ぐため、new URL(target, "http://localhost").pathnameで正規化します。また、リダイレクト前にパス内の制御文字(タブ/改行/ヌル:\t、\n、\0)を拒否します。URL パーサーはこれらを取り除き、/\tevilをプロトコル相対 URL の//evilに変換することがあります。ヌルバイトによってリダイレクトが 500 になる場合もあります。[\x00-\x1F\x7F]内の任意のバイト、バックスラッシュ、およびパーセントエンコードされたスラッシュ/バックスラッシュ(%2f、%5c)を拒否します。 - grep で検索するもの:直接オブジェクト参照、認証ミドルウェアの欠落、リクエストパラメーターから取得したユーザー ID、
redirect(.*from、redirect(.*next、redirect(.*returnTo
A02:暗号化の失敗
-
ソースコードにハードコードされたシークレット、API キー、パスワード
-
弱いハッシュ方式(パスワードに bcrypt/argon2/scrypt ではなく MD5、SHA1 を使用)
-
bcrypt については、コスト係数も確認してください。OWASP の 2024 年のガイダンスでは、12 以上です(コスト 10 の場合、攻撃者にとって約 10ms/コアあたり毎秒 100 ハッシュ)。
-
暗号検証パスでの型強制。 数値解析(
parseInt、Number、parseFloat)は、不正な入力に対して黙ってNaNを返します。また、NaNは<と>のどちらで比較してもfalseになります。タイムスタンプの鮮度チェックif (Math.abs(now - parsed) > tolerance) return falseは、NaNを拒否できません。NaN > toleranceがfalseになるためです。verifySignature/validateToken/署名付き Cookie/JWT クレームのコード内で、parseInt|parseFloat|Number\(.*\)を検索してください。数値を取り出すたびに、不等号による判定の前にif (!Number.isFinite(parsed)) return falseを実行する必要があります。同じ種類の問題として、parseInt('0x123', 10) === 0、parseInt('1e10', 10) === 1、parseFloat('Infinity') === Infinityがあります。 -
ログ、URL、localStorage 内の機密データ
-
保存時または転送時の暗号化の欠如
-
TLS 接続に
VERIFY_PEERを推奨する前に、 デプロイ先の証明書発行者を特定してください。多くのマネージドサービスでは、下位プランで自己署名証明書チェーンが提供されています(Heroku Redis Mini/Hobby、一部の ElastiCache 構成、Supabase のレガシー環境など)。その場合、明示的なca_file:の指定なしではVERIFY_PEERに失敗します。VERIFY_PEERが実際に実行不可能な場合は、優先順位順に次の 3 つの修正オプションを提示してください。- プランをアップグレードするか、CA バンドルを固定する — 証明書検証を復旧します
- リスクを明示的に受け入れる —
VERIFY_NONEを維持し、(a)すべての呼び出し箇所にインラインコメントを付け、(b)代替の制御策(プライベートネットワーク、内部のみのルーティング)を文書化し、(c)再検証の条件を追跡するフォローアップ課題を作成します - ネットワーク経路を制限する — プライベートサブネット/VPC ピアリング/パブリック公開なし
デプロイ先の証明書チェーンが検証可能かどうかを確認せずに、
VERIFY_PEERを安易に推奨してはいけません。 -
汎用的なシークレット名と、既知のプロバイダーキーのプレフィックスを検索してください。
- 汎用:
password、secret、api_key、private_key、MD5、SHA1、base64 - Stripe:
sk_live_、sk_test_、rk_live_、whsec_ - GitHub:
ghp_、gho_、ghu_、ghs_、ghr_ - AWS:
AKIA[0-9A-Z]{16}、ASIA[0-9A-Z]{16} - Google Cloud:
AIza[0-9A-Za-z\-_]{35}、サービスアカウント JSON("type": "service_account") - Slack:
xox[baprs]-、xoxe.xoxp- - OpenAI/Anthropic:
sk-、sk-ant- - Vercel:
vercel_blob_rw_ - バイナリや gitignore 対象のファイルが出力に混入しないよう、
git ls-files | xargs grep -lE 'sk_live|ghp_|AKIA[0-9A-Z]{16}|sk-ant-' 2>/dev/nullを実行してください。
- 汎用:
-
ソースコード以外のファイル拡張子も検索対象に含めてください。 Rails の
cable.yml/database.yml/storage.yml、Kubernetes マニフェスト、Vercel/Netlify のデプロイ設定には、ソースコードのみを対象とした検索では見落とされる TLS や証明書の設定がよく含まれています。VERIFY_NONE/VERIFY_PEERの具体的な検索方法:grep -rn "VERIFY_NONE\|verify_mode" \ --include="*.rb" --include="*.yml" --include="*.yaml" \ --include="*.toml" --include="*.json" \ .
A03: インジェクション
-
SQL インジェクション: 文字列連結による生のクエリ、パラメーター化クエリの欠如
-
NoSQL インジェクション: MongoDB/Convex のクエリにおける、サニタイズされていないユーザー入力
-
コマンドインジェクション: ユーザー入力を使った
exec()、spawn()、system() -
XSS: HTML 内のエスケープされていないユーザー入力、
dangerouslySetInnerHTML、v-html。 -
JSON.stringifyを介したインラインスクリプトのブレークアウト。 サーバーデータをJSON.stringify経由で埋め込む<script type="application/ld+json">や<script>window.__DATA__ = ...</script>は、いずれも脆弱です。JSON.stringifyは<、>、&、U+2028、U+2029 をエスケープしません。</script><script>alert(1)</script>を含む保存済みタイトルは、スクリプトから抜け出します。「内部専用オブジェクト」という前提で問題がないのは、すべてのフィールドがユーザー編集可能な入力から取得されないことが保証されている場合に限られます。- 検索対象:
application/ld+json、__html: JSON.stringify、window.__+JSON.stringify - 修正方法:埋め込みの前に、
<>&\u2028\u2029を対応する\uXXXXUnicode エスケープに置換するエスケープヘルパーで囲みます。
- 検索対象:
-
Rails ERB の危険な出力箇所:
raw()、.html_safe、<%==、許可リストが緩すぎるsanitize、ユーザー入力に対するsimple_format。これらをdangerouslySetInnerHTML/v-htmlと併せて検索します。 -
サニタイザーの選択。 HTML/SVG の XSS 出力箇所を修正する場合、修正には、検証済みのパーサーベースのサニタイザー(JavaScript では DOMPurify/isomorphic-dompurify/sanitize-html、Python では bleach)を必ず使用します。コードレビューでは正規表現ベースのサニタイザーを却下してください。やむを得ず使用する場合、正規表現サニタイザーは次の条件を満たす必要があります。
- 属性名の区切りとして
\sだけでなく[/\s]を扱います。HTML ではタグ名と最初の属性の間に/を使用できます:<img/onerror=…> - SVG 名前空間と HTML 名前空間の両方で危険な要素(
<img>、<body>、<video>、<iframe>)を削除します。HTML 要素は SVG の描画コンテキスト内でもインスタンス化されます。 - 周囲のコンテキストにかかわらず、あらゆる
on*=を削除する最終フォールバック処理を含めます。 - ブラウザ側の最後の防御策として、
Content-Security-Policy: script-src-attr 'none'と併用します。
- 属性名の区切りとして
-
保存型 XSS としての SVG アップロード。 SVG ファイルには
<script>/onloadを含められます。ほとんどの BLOB/オブジェクトストレージは、宣言された content-type でアップロードファイルを配信します。サニタイザー(例:DOMPurify の SVG プロファイル)があり、Content-Disposition: attachmentを指定して配信する場合を除き、アップロード許可リストでimage/svg+xmlを拒否します。 -
書き込み時と描画時の両方でサニタイズします。 保存型 XSS/インジェクションに対しては、信頼境界(DB への書き込み)と描画境界の両方でサニタイズし、多層防御を実現します。保存型 XSS のバグを見つけた場合は、既存データをサニタイズする一度限りのバックフィルマイグレーションを計画します。描画時のみの修正では、不正なデータを含む行が残り、新しい描画経路で再び露出します。
-
Rails の JSON-LD ブレークアウト:
<script type="application/ld+json">内のフィールド値には、j/escape_javascriptを使用しないでください。jは\'と\$を出力します(有効な JS ですが、無効な JSON です)。そのため、アポストロフィまたは$を含むフィールドではJSON.parseが失敗します。代わりに次のイディオムを使用してください。<% schema = { "@context" => "https://schema.org", "@type" => "Article", "headline" => @post.title } %> <script type="application/ld+json"> <%= json_escape(schema.to_json).html_safe %> </script>to_jsonは JSON のエスケープを処理し、json_escapeは</script>によるブレークアウトを防ぐために<>&\u2028\u2029を処理します。ラウンドトリップで検証してください:JSON.parse(json_escape(article.to_json))が元のハッシュと一致することを確認します。 -
テンプレートインジェクション: テンプレートリテラル内のユーザー入力
-
検索対象:
exec(、eval(、innerHTML、dangerouslySetInnerHTML、$where、生の SQL 文字列
A04: 安全でない設計
-
ロジックに欠陥のある認証フロー
-
機密性の高いエンドポイント(ログイン、パスワードリセット、API)でのレート制限の欠如
-
クライアント側でのみ適用されるビジネスロジック上の制約
-
バックグラウンドジョブ/ファイア・アンド・フォーゲットのジョブは、呼び出し元の認証コンテキストを引き継ぎますが、リクエストスコープのガードは失われます。エンキュー時だけでなく、ジョブ内でも認可を再確認してください。検索対象:
Promise.all(...).catch(、void someAsync(、.catch(noop)、ワーカー内で再認証せずに呼び出されるキューのenqueue(。 -
関連ルートの監査。 あるハンドラーでステートマシンまたは不変性のガード(例:
PUT /api/foo/[id]におけるWHERE … AND signedAt IS NULL)を見つけたら、同じテーブルを書き込む他のすべてのハンドラーを検索します。rg 'update\(\s*tableName\b|\.update\(tableName' --type ts -B1 -A8すべての呼び出し箇所で、同じガード、同じ
userIdの述語、同じ競合処理(returning()と 0 行チェック)が必要です。よくある見落としは、PUTを強化した後に再監査されないままリリースされるPOST /:id/sendやPOST /:id/convertルートです。 -
課金に影響する外部リソース作成の TOCTOU。 「チェックのために
SELECTを実行し、次にprovider.create()を呼び、その後 INSERT で新しいリソース ID を記録する」ハンドラーは、並行実行時にプロバイダー側で孤立リソースを作成する可能性があります。Stripe アカウント、Auth0 / Clerk ユーザー、SendGrid テンプレート、S3 バケットは、ID を保存したかどうかにかかわらず、すべて課金対象になるか、クォータを消費します。次のパターンで修正します。- まず
INSERT … ON CONFLICT DO NOTHINGで確保します(DB の UNIQUE 制約がロックになります) - プロバイダーを呼び出します
- 楽観的ガードを使って保存します。
UPDATE … SET externalId = ? WHERE externalId IS NULLを実行し、0 行かどうかを確認します - 競合に負けた場合は、ベストエフォートで
provider.delete(id)を使って孤立リソースをクリーンアップし、クリーンアップに失敗した場合はログに記録します
- まず
-
ワーカキューの状態遷移にはアトミックな確保が必要です。 保留中の行をポーリングする cron / ワーカーは、処理前に各行をアトミックに確保する必要があります。
SELECT+process()+UPDATEは競合状態を引き起こします。2 つのワーカー(または重複して実行された cron)が同じ保留中の行を確認し、両方が外部呼び出しを行って重複配信が発生します。修正方法:UPDATE … SET status='processing' WHERE id=? AND status='pending' RETURNING …。Postgres のRETURNINGを使えば、1 回の往復で確保と読み取りができます。UPDATE が 0 行を返した場合は、別の処理がすでに確保しています。より高スループットのキューには、代替としてSELECT … FOR UPDATE SKIP LOCKED(Postgres / Cockroach)を使用できます。 -
マルチテナントの Webhook 署名照合。 認証されていない Webhook エンドポイントが、テナントごとのシークレットを順番に試してテナントを特定する場合、無意味なリクエストを含むすべてのリクエストで O(N) 回の DB 参照と O(N) 回の HMAC 計算が発生します。攻撃者はランダムな署名を大量に送信し、認証を通過することなく CPU / DB 負荷を増幅できます。次の対策を組み合わせてください。
- DB にアクセスする前に署名形式で事前フィルタリングします。プロバイダーが送信する正確な長さと文字セットに一致しない署名を拒否します(例:HMAC-SHA256 の 16 進数表記には
/^[a-f0-9]{64}$/i) - リクエストごとの署名チェックに上限を設けます(例:
LIMIT 200) - エンドポイントに対してIP ごとのレート制限を設けます
- プロバイダーが対応している場合は、Webhook URL にテナント ID を含めます(
/api/webhooks/foo/<connection_id>)。これにより検索を O(1) にできます
- DB にアクセスする前に署名形式で事前フィルタリングします。プロバイダーが送信する正確な長さと文字セットに一致しない署名を拒否します(例:HMAC-SHA256 の 16 進数表記には
-
レート制限キーのフォールバック。 レート制限キーに攻撃者が制御できる、または欠落する可能性のある識別子(IP、user-id、session-id)が含まれている場合、存在しないときに共有の定数文字列へフォールバックしてはいけません。代わりに、(a)リクエストを拒否する、(b)攻撃者が共有できないリソース単位の識別子にフォールバックする(サインアップにはメールアドレスごと、課金には Stripe 顧客ごと)、または(c)明示的にフェイルオープンにしてログに記録します。共有の
'unknown'/'anon'バケットはロックアウトを引き起こす要因です。攻撃者がそのバケットを占有すると、そのプロキシ経路を利用するすべてのユーザーがロックアウトされます。 -
設定済みだが未ロードの確認。 セキュリティミドルウェア(レート制限、認証、CSRF、スロットリング)が「すでに設定済み」と宣言する前に、初期化ファイルが存在するだけでなく、gem / パッケージが実際にインストールされていることを確認してください。
if defined? Foo/if PACKAGE in sys.modulesで囲まれた初期化処理は、パッケージが含まれていない場合に何もせず終了します。スタック 確認方法 Ruby/Rails grep -E "^ GEM_NAME " Gemfile.lock(4 つのスペースのインデントはトップレベルの gem を示します)Node grep "\"PACKAGE_NAME\":" package-lock.jsonまたはnode -e "require('PACKAGE_NAME')"Python pip show PACKAGE_NAMEGo grep PACKAGE_PATH go.sumRails の場合:ミドルウェアが実行時スタックに含まれていることも確認してください。
bundle exec rails middleware | grep -i FOO。
A05:セキュリティ設定ミス
-
本番環境の設定でデバッグモードが有効になっている
-
CORS ポリシーの許可範囲が広すぎる(
Access-Control-Allow-Origin: *) -
HTTP セキュリティヘッダーが設定されていない(CSP、HSTS、X-Frame-Options、X-Content-Type-Options)
-
デフォルトの認証情報や設定が含まれている
-
スタックトレースや内部情報を露出する詳細なエラーメッセージ — 検証ライブラリがスキーマの詳細(例:Zod の
err.issues、Joi のエラーツリー)をクライアントに返すケースも含みます -
ヘッダーの基本設定値(貼り付けて調整してください):
ヘッダー 値 Strict-Transport-Securitymax-age=63072000; includeSubDomains; preload— preload の設定前に、すべての*.example.comが HTTPS を提供していることを確認してくださいX-Content-Type-OptionsnosniffX-Frame-OptionsDENY(CSP のframe-ancestorsと併用する多層防御)Referrer-Policystrict-origin-when-cross-originPermissions-Policycamera=(), microphone=(), geolocation=(), browsing-topics=()—interest-cohortは以前の FLoC 名(Chrome ≤ 100)です。現在の Chrome ではbrowsing-topicsを使用しますContent-Security-Policyまず frame-ancestors 'self'を設定します。完全な CSP の設定には、サイトごとのスクリプト監査(インラインの<style>、JSON-LD、分析ツール)が必要ですHSTS の preload 登録は取り消しにくく、解除には数か月かかります。申請前に確認してください。
-
フレームワーク別のセキュリティヘッダーの設定場所:
- Next.js:
next.config.{js,ts}のheaders()ブロック、またはvercel.jsonのheaders - Rails:
config/initializers/secure_headers.rb、config/application.rb - Express:
app.use(helmet()) - Django:
settings.pyのSECURE_*設定
- Next.js:
-
ランタイムと API の不一致。 Edge/Workers/V8 分離ランタイムで実行されるコードは、Node 専用モジュールを読み込めません。Next.js の
middleware.ts/Cloudflare Workers/Vercel Edge 関数内でnode:crypto、node:fs、node:buffer、node:netなどをインポートすると、コンパイルは正常に完了しますが、最初のリクエストでFailed to load external moduleが発生します。ミドルウェアと Edge 指定のルートを監査し、Node 専用のインポートがないか確認してください。移植性の高いコードには Web Crypto(crypto.subtle)を使用してください -
Rails 管理エンジンのマウント。
config/routes.rbを検索して、エンジンやダッシュボードのマウント(PgHero、Sidekiq::Web、Flipper UI、Mission Control、Audit1984)を確認し、対応するイニシャライザーの認証ミドルウェアが、本番環境だけでなく、インターネットからアクセス可能な すべての 環境に適用されていることを確認してください。grep -E "mount .+::(Engine|Web|UI)|mount .+::App" config/routes.rb grep -rn "Rails.env.production?" config/initializers/ | \ grep -B1 -A5 "Auth::Basic\|authenticate\|secure_compare"PgHero、Sidekiq::Web、Flipper、Mission Control の README の例では、いずれも
if Rails.env.production?で認証を囲んでいるため、ステージング、レビューアプリ、プレビュー環境で管理 UI が認証なしで公開されます。修正方法:ガードをunless Rails.env.local?に変更します(Rails 7.1 以降でdevelopment || testを判定するヘルパー)。また、認証用の環境変数が未設定の場合にアクセスを拒否する、フェイルクローズのチェックを追加してください。 -
有料エンドポイントでの並行実行による競合。 エンドポイントが有料の下流処理(LLM 呼び出し、サードパーティ API、スクレイピングジョブ)を起動する場合、間にアトミックな取得処理を挟まずに
SELECTの後でrunX()を呼び出していないか、また無条件のUPDATE … SET status='processing'を実行していないか確認してください。並行する 2 つのリクエストがどちらも読み取り側のチェックを通過し、両方とも処理を実行する可能性があります。修正方法:条件付きのUPDATE … WHERE id = ? AND status = 'pending' RETURNING …、または Postgres のアドバイザリロックを使用します。ポーリングや再試行でクォータを消費しないよう、取得に成功した場合にのみレート制限の枠を消費してください。
(注:この箇条書きが A05 に含まれるのは、失敗が不適切に設定された不変条件ガードとして現れるためです。競合パターン自体は、説明の仕方によって A04/A05/A08 にまたがります。)
-
ソースツリーの整理。 同期競合による重複ファイルや不要なコードを検索してください。レビュー担当者が正規のファイルを修正しても、脆弱なコピーが残る可能性があります。
find . \( -name '* 2.*' -o -name '* 3.*' -o -name '*.orig' \ -o -name '*~' -o -name '*.bak' \) -not -path '*/node_modules/*'検出事項は A05 として扱ってください。正規ファイルにパッチが適用されても、重複ファイルには脆弱性が残る場合があります。
-
Next.js の
headers()ルールのマージ。next.config.tsのheaders()のルールはルートごとに適用され、マージされます。より具体的なルールでも、明示的に再宣言していないヘッダーは上書きされません。/:path*のデフォルトルールでframe-ancestors 'none'とX-Frame-Options: DENYを設定し、/embedのオーバーライドルールでframe-ancestors *を設定すると、埋め込みルートにはframe-ancestors *とX-Frame-Options: DENYの両方が設定されます(矛盾しており、古いブラウザーではフレーミングが機能しない場合があります)。デプロイ済みのオリジンに対してcurl -Iで確認してください。設定の確認だけではマージを見落とします。すべてのルールで XFO を設定するか、XFO を完全に削除してください(最新のブラウザーでは CSP のframe-ancestorsが優先されます)。 -
Bearer/HMAC ルートを除外しない認証ミドルウェア。 Cron ジョブ、Stripe / GitHub の Webhook、Bearer トークンまたは HMAC で認証するルートは、セッション Cookie のミドルウェアから除外する必要があります。症状としては、デプロイやスケジュール実行のたびにこれらのルートが気付かれないまま
/loginに 302 リダイレクトされ、ルートレベルの署名チェックが実行されず、連携が機能しているように見えても、ある時点で機能しなくなります。 -
未設定の環境変数を展開する Bearer トークンの比較。 存在確認をせずに
process.env.Xを展開する比較(\Bearer ${process.env.WEBHOOK_TOKEN}`)は、環境変数が未設定の場合、リテラル文字列"Bearer undefined"になります。環境変数が未設定であることを推測した攻撃者は、このリテラル文字列を再利用できます。モジュールの読み込み時に存在を確認してください。if (!process.env.WEBHOOK_TOKEN) throw new Error(...)` -
JSON の 401 ではなく HTML の 302 リダイレクトを返す API ルート。 未認証のリクエストをすべて
/loginに 302 リダイレクトする認証ミドルウェアは、fetchクライアントを妨げ(リダイレクトに従って HTML を受け取る)、監視で認証状態を把握しにくくし、302 と 404 の違いによって攻撃者にエンドポイントの存在を知らせます。API ルートは、機械可読な本文を含む401 application/jsonを返す必要があります。
A06: 脆弱なコンポーネント
npm audit(Node)、pip audit(Python)、または同等のコマンドを実行します- ロックファイルを確認し、既知の脆弱な依存関係のバージョンがないか調べます
- 重大な CVE がある依存関係を報告します
npm auditとあわせてnpm audit --omit=devを実行し、到達可能性に基づいてトリアージします:- 実行時に到達可能(
dependencies内)— 必ず修正します - ビルド時のみ(Vite、drizzle-kit 経由の esbuild、postcss)— 通常は後回しにします
- 開発時のみ(リンター、テストライブラリ)— 後回しにします
- 実行時に到達可能(
- CVE の全体像を把握し、到達可能性に基づいてトリアージするには、
dependency-auditを実行してください。ここでの A06 は、1 行の簡易確認です。
A07:認証の失敗
- 弱いパスワードポリシー
- セッション管理の問題(
secure/httpOnlyフラグの欠如、有効期限なし、ローテーションなし) - 認証情報をそのままCookieに格納: Cookieの値そのものが認証情報です(例:
cookies.set("admin_token", process.env.ADMIN_PASSWORD)とし、読み取り時に一致を確認する)。httpOnlyとsecureが設定されていても、これは平文の認証情報を保存する方法(CWE-522)であり、ローテーションや失効にも対応していません。crypto.subtle.verify/crypto.timingSafeEqualで検証する、HMAC署名付きの有効期限付きトークンに置き換えてください。 - 認証情報の比較が定時間でない: パスワード、APIキー、署名に対する
submitted === expectedは、タイミングから長さや先頭部分の一致を漏えいさせます。crypto.timingSafeEqual(Node)またはcrypto.subtle.verify(Web Crypto)を使用してください。 - ログイン時のレート制限の欠如(認証情報のスタッフィング攻撃のリスク)
- パスワードリセットフローの不備
- Rails/Devise —
password_lengthには:validatableが必要です。config/initializers/devise.rbのconfig.password_lengthは、Userモデルのdevise :...宣言に:validatableが含まれている場合にのみ適用されます。devise :database_authenticatable, :registerable, :recoverable, :rememberable(:validatableなし)を宣言したモデルでは、initializerの設定にかかわらず、任意の長さのパスワードと任意の形式のメールアドレスが受け入れられます。検索対象:app/models/内の^\s*devise\s+:。:validatableがない行を検出してください。既存アプリに追加すると、作成時と更新時に検証が適用されますが、既存の弱いパスワードは遡って無効化されません。 - NextAuth v5/Auth.js の注意点:
AUTH_SECRETが未設定の場合、弱い開発用の値が警告なしに導出されます。モジュールの読み込み時に存在を確認してください:if (!process.env.AUTH_SECRET) throw ...- Credentialsプロバイダーの
authorize()には組み込みのレート制限がありません。ラッパーを追加するか、上流に制限機能を追加してください。そうしないと、認証情報のスタッフィング攻撃が容易になります。 - サインアップのエラーメッセージ(「メールアドレスはすでに存在します」など)によるアカウントの有無の列挙:一律に「メールを確認してください」と返してください。
- JWT戦略とDrizzleAdapter/PrismaAdapterの併用:この組み合わせではアダプターはほぼ何もしません。失効処理は明示的に設計する必要があります。
- 次のパターンを検索してください:
cookies.set\(.*process\.env,=== .*PASSWORD,!== .*SECRET,!== .*Bearer.*\${
A08:データ整合性の失敗
- ユーザー入力の安全でないデシリアライズ
- CI/CDパイプラインでの整合性チェックの欠如
- ロックファイルの整合性検証(SRIハッシュ)の欠如
- 状態を確認せずに外部リソースを上書きする。 プロバイダー側のリソース(Stripeの支払いインテント、サブスクリプション、Webhookサブスクリプションなど)を作成し、そのIDをDBの行に保存するハンドラーは、既存のリソースがまだ処理中かどうかを確認してから上書きしてください。古い
clientSecret/トークンをクライアントがまだ保持している可能性があります。完了時にWebhookハンドラーがIDの変更によって行を見つけられず、支払いは処理されたのにDBの状態は永久にprocessingのままになることがあります。対処法:新しいリソースを作成する前に、既存のリソースを取得してください。状態が終端状態ではなく(processing、requires_payment_method、requires_action)、パラメーターに変更がなければ、それを再利用して既存のclientSecretを返してください。新しいリソースを作成するのは、既存のリソースがキャンセル済み/成功済みの場合、またはパラメーターが変更された場合に限ります。 - DBの状態を永続化する前に外部副作用を実行する。 ハンドラーがDBへの書き込みと外部副作用(メール送信、課金、Webhook、S3への書き込み)の両方を行う場合、外部呼び出しはDB書き込みのコミット後に実行してください。「DBの永続化後に外部処理を再試行する」失敗パターンは復旧可能ですが、「外部処理の実行後にDBの状態が古いままになる」パターンは復旧できません。対処パターン:先に予約し、その後に実行します。事前の状態(
WHERE status='draft')を条件として、行の状態をアクション後の状態に変更する条件付きUPDATEを発行してください。更新件数が0件なら拒否します。1件ならプロバイダーを呼び出します。プロバイダーが失敗しても、行にはすでに意図が反映されています。アラートを出し、再試行してください。さらに、この方法でハンドラーも冪等になります。
A09:ロギングとモニタリングの失敗
- 認証イベント(ログイン、失敗、権限の変更)が記録されていない
- 機密データ(パスワード、トークン、PII)がログに書き込まれている
- 不審なパターンに対するアラートがない
- エラーが黙って握りつぶされる。 空の catch ブロックは、バグと攻撃の兆候(レートリミッターの停止、デシリアライズエラー、認証の失敗)の両方を隠します。
- 次を grep してください:
catch \{\}、catch (_) \{\}、catch (_e) \{\}、.catch(() => {})、.catch(() => null)、try { ... } catch { return null } - 対策:最低限、PII を含めずにエラーのカテゴリ(
error.name、ステータスコード)を記録してください
- 次を grep してください:
- 認証されていないエンドポイントから、検証済みドメインを使って、ユーザーが指定したアドレスにメールを送信している。 認証なしでアクセスできるハンドラーが、攻撃者の指定した
to:アドレスに送信メール(確認メール、後から申請できるパスワードリセット、招待、「お問い合わせを受け付けました」など)を送る場合、特に攻撃者が件名や本文に影響を与えられると、検証済み送信者の配信評価を悪用したフィッシングにつながります。対策(いずれかを選択してください):- 自動で確認メールを送信しない — 人間が返信したときに返信が行われます
- 返信メールを送信する前に、所有権の証明を必須にする(確認リンク/ダブルオプトイン)
- 攻撃者が件名や本文に影響を与えられない、簡素なテンプレートを使った別の
noreply@送信者を使う
A10:SSRF
- ユーザーが制御できる URL がサーバー側の HTTP リクエストに渡されている
- URL の検証と許可リストが不足している
- ホスト名しか確認しない許可リスト — 実際に有効な許可リストでは、次のすべてを拒否する必要があります:
- 不正なスキーム:
http://allowed.com/(https:のみを想定している場合) - 埋め込み認証情報:
https://user:[email protected]/ @を使ったホスト名の偽装:https://[email protected]/(ホスト名はevil.comに解決されます)- デフォルト以外のポート:
https://allowed.com:8443/ - Punycode/IDN による偽装:
https://аllowed.com/(キリル文字の а)またはxn--llowed-pdc.com - 末尾のドット:
https://allowed.com./(DNS 上は同等ですが、文字列比較では見落とされがちです) - サブドメインの混同:
https://allowed.com.evil.com/ - 角括弧で囲まれた IPv6 リテラル:
https://[::1]/ - そのままの IPv4:
https://127.0.0.1/ - 10 進整数形式の IPv4:
http://2130706433/→ 127.0.0.1 - 16 進数形式の IPv4:
http://0x7f000001/ - 8 進数形式の IPv4:
http://0177.0.0.1/、ゼロ埋めされた0010.0.0.1/→ 8.0.0.1(8 進数!) - IPv4 マップ IPv6:
http://[::ffff:127.0.0.1]/→::ffff:*の範囲全体をブロックする - 末尾にドットが付いたホスト名:
http://localhost./、http://metadata.google.internal./ - クラウドメタデータのエンドポイント:AWS
169.254.169.254、GCPmetadata.google.internal、ECS169.254.170.2 - CGNAT 範囲:
100.64.0.0~100.127.255.255 - リンクローカル IPv6:
fe80::/10、ユニークローカル IPv6:fc00::/7
- 不正なスキーム:
- フェッチ時のガード:
redirect: "error"(攻撃者が制御するリダイレクトをたどらない)、明示的なタイムアウト、メタデータサービスへの 3xx リダイレクトをたどらない - 検証とフェッチの間にある TOCTOU に注意してください — DNS はその間に異なるアドレスへ解決される可能性があります(DNS リバインディング)。リスクの高い呼び出し元では、解決済み IP を固定して
Host:ヘッダーを使って IP に直接接続するか、検証済みのプロキシを使用してください - 画像オプティマイザーをプロキシとして使用(Next.js、Nuxt、SvelteKit):
images.remotePatterns: [{ hostname: '**' }]、domains: ['*']、またはワイルドカードを含む設定があるnext.config.{ts,js}を使うと、攻撃者が任意の URL へのアクセスを CPU/帯域幅経由で実行できてしまいます。- 次を grep してください:画像設定内の
remotePatterns、domains: - 対策:既知の特定のホスト名に固定してください。画像がすべてローカルにある場合は空のままにします。
- 次を grep してください:画像設定内の
- ユーザー入力を伴う
fetch(、axios(、http.get(、urllib、requests.get(を grep してください
実行時に修正を検証する
修正を適用したら、影響を受けるコードパスを実行してください。型チェックやビルドだけで終わらせてはいけません。最新のフレームワークには、コンパイルは正常に通るものの、実行時にのみ発生する障害モードがあります。
- Edge/Node の分離ランタイム。 Next.js middleware、Cloudflare Workers、Vercel Edge では、Node 専用のインポート(
node:crypto、node:fs)が正常にビルドされても、最初のリクエストで例外をスローします。 - 遅延モジュール読み込み。
import()や実行時 DI で読み込まれるアダプターやプラグインは、該当するコードパスを実行したときにのみ問題が表面化します。 - 環境変数のフォールスルー。 X が未設定の状態で
Bearer ${process.env.X}を使用すると、テスト環境では X が定義されているためテストで検出されず、文字列がそのまま使われます。
リリースする修正ごとに、影響を受けるルートまたはジョブを実行し、レスポンスを記録してください。tsc --noEmit とビルドが成功しても、修正が検証されたことにはなりません。
XSS/サニタイザー設定の修正では、標準のペイロードセットを設定済みのポリシーに適用し、それぞれが無害化されることを確認してください。
<img src=x onerror=alert(1)>
<a href="javascript:alert(1)">x</a>
<a href="data:text/html,<svg onload=alert(1)>">x</a>
<img srcset="javascript:alert(1) 1x,https://ok.com/a.png 2x">
<svg><script>alert(1)</script></svg>
<math><mtext></style><img src=x onerror=alert(1)></math>
<a href="//evil.com">protocol-relative</a>
<svg></svg><img/onerror=alert(1) src=x>
セカンドオピニオンによる確認
1 回目の監査ではチェックリストにあるカテゴリを確実に検出できますが、チェックリストに含まれていない具体的な回避方法(localhost. と localhost の違い、IPv4 マップド IPv6、順序付けられた 404/400/401 を利用したステータス列挙、コールバック URL の制御文字、有料エンドポイントでの同時実行競合など)は見落とされがちです。
最初のレポートを作成した後、および修正を適用した後に、明示的に敵対的な視点(「作成者が過信していると想定し、見落としを探す」)で 2 回目の確認を行ってください。相関した盲点を避けるため、できれば別のモデルまたはエージェントを使用してください。1 回目の確認との不一致は、より価値の高い指摘として扱います。
2 回目の確認でよく見つかる項目:
- 修正自体が新たに生み出した攻撃対象領域(除外されたルートを利用した認証バイパス、新しいクエリによって生じた IDOR)
- 書き換え後に古くなったコメント
- 新しいコードの境界条件(環境変数未設定時のフォールスルー、空の入力)
- 修正とレポートの間のドキュメントの乖離
- サードパーティライブラリを設定する修正。 修正がライブラリ自身のドキュメントにあるコードスニペット(認証、暗号、HTTP クライアント、レート制限ミドルウェア)である場合、そのスニペットが正しくても、コードパス上で実行されないことがあります。修正済みと判断する前に、固定されたバージョンのライブラリを grep し、呼び出し元から設定が影響を与えるコードパスまでを追跡してください。例:プログラムから
auth.api.signInEmail(...)を呼び出している場合、Better Auth のrateLimitを有効にしても効果はありません。レート制限が適用される HTTP ルーターを経由しないためです。
レポート形式
OWASP の各カテゴリについて、次の 3 つの状態のいずれかを記録してください。
- 指摘事項(重大度と修正方法を記載)
- 問題なし — grep した対象を明記したうえで、「X を確認しましたが、問題は見つかりませんでした」と明示してください。
- 該当なし — そのカテゴリが該当しない理由を説明してください(例:「A07 は該当なし:このコードベースには認証対象がありません」)。
エグゼクティブサマリーに「確認済みで問題のなかった項目」セクションを含めてください。監査の信頼性は、指摘事項の一覧だけでなく、調査したことを証明することで得られます。
指摘事項には、次の 3 つの対応状況があります。
- 修正済み — この PR で修正をリリース済みです。検証待ちの状態でクローズします。
- 延期 — 修正の必要性を認識し、対応を予定しています。次回のデプロイを延期対象の修正に合わせて行う必要があるか(リリースブロッカー)、または合わせなくてもよいか(カレンダーに基づく修正を予定した許容可能なリスク)を明記してください。延期を決めても重大度は変わりません。
- 受容リスク — 現在の設定では修正を予定していません。レポートには次の項目を記録する必要があります。
- 修正が適用できない理由 — コスト区分、依存関係のバージョン制約、デプロイのトポロジー、ベンダーの制限
- 代替の緩和策 — プライベートネットワーク、署名付き Cookie、内部限定のルーティングなど
- 再評価の条件 — どのような条件(プランのアップグレード、依存関係の更新、トラフィックパターンの変化など)があれば、この指摘事項を「受容リスク」の状態から外すのか
この 3 項目をすべて含まない「受容リスク」の記載は、別のラベルの下で実際の指摘事項が黙って除外されている状態です。
指摘事項ごとに、次の内容を記録してください。
#### [SEVERITY] A0X: [Title]
**File:** `path/to/file.ts:42`
**CWE:** CWE-XXX
**Description:** [What the vulnerability is and why it matters]
**Vulnerable Code:**
[code snippet]
**Remediation:**
[Fixed code snippet with explanation]
**Verification:** Concrete adversarial input + the command or code path that proves the fix holds. For XSS: the script-tag breakout payload that no longer breaks out. For open redirects: the off-host URL that now rejects. For password-length: the 1-char password that now fails to save. "The linter says it's fine" is not verification — static analysis has known blind spots for correctness bugs that happen to also be security fixes.
エグゼクティブサマリーを作成してください。
# Security Audit Report
## Project: [name]
## Stack: [technologies]
## Date: [date]
### Summary
- Total findings: X
- Critical: X | High: X | Medium: X | Low: X | Info: X
### Findings
[Individual findings as above]
### Prioritized Remediation Plan
1. [Critical fixes — immediate]
2. [High fixes — this week]
3. [Medium/Low — scheduled]
境界
- ユーザーが提供した、または確認するよう指定したコードのみを監査します
- エクスプロイトではなく修正案を提示し、必ず修復方法を含めます
- 確信度の低い指摘は、確定事項ではなく「Potential」として示します
- コードベースが大きすぎて全体を監査できない場合は、認証、入力処理、データアクセス層を優先します
- バックドアの挿入やセキュリティ制御の弱体化を求める依頼は拒否します
参考資料
- OWASP Top 10 (2021)
- OWASP コードレビューガイド
- CWE Top 25