Webセキュリティ対策ガイド:6つの層で守るベストプラクティス(2026年版)
ポイント
Webセキュリティ対策は層で考えるのが基本です。ドメインとDNSを固め(レジストラロック、DNSSEC、CAA)、HSTSで最新のTLSを強制し、厳格なHTTPセキュリティヘッダーを送信します。パスワードはArgon2idでハッシュ化したうえでMFAとレート制限で守り、入力の検証とアクセス制御はサーバー側で行います。依存関係はバージョンを固定して監査し、提供していないポートはすべて閉じ、侵害に気づけるだけのログを残しましょう。以下の各対策には、具体的な設定と、それが本当に有効になっているかを無料で確認する方法を添えています。テストしていない対策は、存在しないのと同じだからです。
今すぐ確認

読み始める
- Webセキュリティ対策のベストプラクティスとは
- Webセキュリティの全体像:守るべき6つの層
- レイヤー1:ドメインとDNSを守る
- レイヤー2:HTTPSとTLSを正しく設定する
- レイヤー3:HTTPセキュリティヘッダー
- レイヤー4:クレデンシャルスタッフィングに耐える認証
- セッション、Cookie、CSRF
- インジェクションとXSS:入力を検証し、出力をエンコードする
- アクセス制御の不備(Broken Access Control):第1位のリスク
- レイヤー5:依存関係とソフトウェアサプライチェーン
- レイヤー6:サーバーのハードニング(ポート、パッチ、シークレット、バックアップ)
- メール認証:SPF、DKIM、DMARC
- ログを取り、監視し、侵害に備える
- テストする:スキャナー、ペネトレーションテスト、継続的なチェック
- Webセキュリティ対策チェックリスト
- OWASP Top 10:2025との対応表
- Webセキュリティ対策のよくある質問
Webセキュリティ対策のベストプラクティスとは
Webセキュリティ対策のベストプラクティスとは、Webサイトやwebアプリケーションが乗っ取られたり、改ざんされたり、訪問者への攻撃に悪用されたり、気づかないうちにデータを抜き取られたりするのを防ぐための一連の対策です。1つの製品や1つの設定で完結するものではありません。ドメインレジストラから始まり、DNS、TLS、サーバーが送るHTTPヘッダー、ログインや入力を処理するコード、土台となるサードパーティのパッケージ、サーバーそのもの、そして問題の発生を知らせてくれるログまでを貫く、判断の積み重ねです。
レイヤーで考える理由は、攻撃者がそう考えているからです。Verizonの2026 Data Breach Investigations Reportは、2024年11月から2025年10月までの2万2,000件超の侵害を対象に、侵害の31%が悪用されたソフトウェアの脆弱性から始まったと報告しています。前年の20%から増え、初めて盗まれた認証情報(13%)を上回りました。フィッシングは16%で横ばいです。ランサムウェアは侵害の48%で確認され、前年の44%から増えました。パッチ1つの適用漏れ、漏えいしたAPIキー1つ、レート制限のない管理ページ1つで十分です。このガイドの対策に特殊なものはありません。侵害されるサイトの多くは、あるレイヤーを磨き込む一方で、別のレイヤーの基本を飛ばしていたサイトです。
このガイドは外側から内側へ順に構成しています。各対策では、なぜ重要か、具体的に何を設定するか、どう確認するかを、コマンドやHTTPヘッダーチェック・SSLチェックのような無料ツールで示します。有効だと思い込んで一度も確認していない設定は、何も守ってくれないからです。
Check a site's security headers
Webセキュリティの全体像:守るべき6つの層
Webサイトへの攻撃は、必ず6つの層のどれかを狙います。以下の表は、このガイド全体の地図です。各層に何があり、典型的にどう攻撃され、どの無料チェックで防御が実際に機能しているかを確認できるかをまとめました。
| 層 | 攻撃者がここで行うこと | 主な対策 | 確認方法 |
|---|---|---|---|
| 1. ドメインとDNS | ドメインの乗っ取り、ネームサーバーの書き換え、放置されたサブドメインの乗っ取り、不正な証明書の取得 | レジストラロック、MFA、DNSSEC、CAA、古いレコードの棚卸し | WHOIS検索、DNSヘルスチェック、サブドメイン検索 |
| 2. 通信(TLS) | HTTPへのダウングレード、暗号化の除去、古い暗号スイートの悪用、期限切れの証明書 | TLS 1.2以上、HSTS(プリロードは任意)、自動更新 | SSLチェッカー |
| 3. HTTPヘッダー | スクリプトの注入(XSS)、クリックジャッキングのためのフレーム埋め込み、コンテンツタイプの推測、リファラーの漏えい | CSP、frame-ancestors、nosniff、Referrer-Policy、Permissions-Policy | HTTPヘッダーチェッカー |
| 4. アプリケーション | クレデンシャルスタッフィング、インジェクション、アクセス制御の不備、CSRF、安全でないアップロード | Argon2id+MFA+レート制限、パラメータ化クエリ、サーバー側での認可、SameSite Cookie | コードレビュー、ZAP、パスワード強度テスト |
| 5. 依存関係 | 悪意あるパッケージ、ライブラリの既知のCVE、CIパイプラインの侵害 | ロックファイル、監査、バージョン固定、SBOM、最小権限のトークン | npm audit、Dependabot、Trivy |
| 6. サーバーと運用 | 開いたままのポート、初期パスワード、未適用のパッチ、バックアップなし、ログなし | ファイアウォール、鍵認証のみのSSH、パッチ適用、3-2-1バックアップ、ログの集中管理 | ポートチェッカー、IPブラックリストチェッカー |
上から順に進めてください。レイヤー1〜3は午後の数時間で終わる設定作業で、すべてのページを一度に守れます。レイヤー4〜6は継続的な習慣であり、コードレビューとデプロイのプロセスに組み込んでおく必要があります。
レイヤー1:ドメインとDNSを守る
このガイドのほかのすべての対策は、あなたがまだ自分のドメインを管理できていることを前提にしています。攻撃者がレジストラにログインしたりネームサーバーを書き換えたりできれば、トラフィックを好きな場所へ向け、あなたのドメイン名で有効な証明書を取得し、あなた宛てのメールをすべて読めてしまいます。その間、あなたのアプリケーションのコードは一度も実行されません。だからこそドメインとDNSのセキュリティはWebセキュリティ対策の第一歩であり、その大部分は設定だけで完了します。
まず自分のドメインでWHOIS検索を行い、3つの点を確認してください。ステータスコードにclientTransferProhibitedが含まれていること、有効期限が1年以上先であること、そしてレジストラが実際に自分でアカウントを持っているところであることです。企業のドメインが、以前の外注業者のレジストラアカウントに残ったままになっているケースは意外なほどよくあります。表示されるステータスコードの意味は、WHOIS検索ガイドですべて解説しています。
レジストラロック、MFA、有効期限アラート
レジストラロック(トランスファーロックとも呼ばれます)を有効にして、明示的に解除しない限りドメインをほかのレジストラへ移管できないようにし、レジストラのアカウント自体でも多要素認証(MFA)をオンにしましょう。1つのログインで下流のすべてを支配できるため、レジストラのアカウントはフィッシングの格好の標的です。レジストラが対応していれば、SMSではなくハードウェアキーや認証アプリを使ってください。
支払い方法が失効しない設定でドメインを自動更新にし、それでも有効期限の60日前にカレンダーで通知を設定してください。失効したドメインは期限の当日に失われるわけではありませんが、猶予期間と償還期間が過ぎると(.comでは最大約80日)削除され、ドロップキャッチサービスが解放直後の数秒で登録します。購入者はあなたのメール、トラフィック、そしてあなたのドメインを信頼しているOAuthログインをそのまま引き継ぎます。
- レジストリロック(レジストリ側で手動かつ帯域外で行うロック)は、事業が依存しているドメインなら追加料金を払う価値があります。レジストラのアカウントが侵害されても、ネームサーバーの変更を防げます。
- 役割を分けます。 ドメインの費用を払う人、ドメインを所有するアカウント、DNSプロバイダーをそれぞれ文書化し、特定の社員のメールボックスに紐づけないようにします。
- WHOISプライバシーを有効にして、連絡先情報がフィッシングの地図にならないようにします。アカウントには
domains@のような、監視されている役割アドレスを登録しておきましょう。
DNSSECでゾーンに署名する
DNSSECはDNSゾーンに署名して、リゾルバーが偽造された応答を検出できるようにします。これがないと、リゾルバーのキャッシュを汚染できる攻撃者が、ブラウザにあなたのドメイン名を表示したまま訪問者を偽のサーバーへ誘導できます。マネージドDNSの多く(Cloudflare、Route 53、Google Cloud DNS、多くのレジストラ)は、スイッチ1つで有効にできます。手作業が必要なのは、レジストラでDSレコードを公開する手順だけです。digか、DSレコードと署名を確認するDNSヘルスチェックで検証してください。
dig +dnssec +short yourdomain.com A
# A signed zone returns the record AND an RRSIG line:
# 203.0.113.10
# A 13 2 3600 20270115000000 20261225000000 34505 yourdomain.com. Kx3f...==
dig +short yourdomain.com DS
# A DS record at the parent zone proves the chain of trust is complete
# 2371 13 2 6A1B...C9CAAレコードで証明書の発行元を制限する
CAAレコード(RFC 8659)は、どの認証局(CA)があなたのドメインの証明書を発行してよいかを指定します。すべての公的CAは発行前にこれを確認する義務があるため、CAAレコードがあれば、別のCAのドメイン認証をだました攻撃者を締め出せます。一般的なケースは次の3つのレコードでカバーできます。証明書を発行できるCA、ワイルドカード証明書を発行できるCA、そして拒否された発行リクエストの報告先です。
yourdomain.com. CAA 0 issue "letsencrypt.org"
yourdomain.com. CAA 0 issuewild ";"
yourdomain.com. CAA 0 iodef "mailto:security@yourdomain.com"古いレコードを棚卸しする:サブドメイン乗っ取り
サブドメインテイクオーバーは、使わなくなったサービスをDNSレコードがまだ指しているときに起こります。たとえば、削除されたGitHub PagesサイトやAzureアプリ、S3バケット、SaaSベンダーのホスト名を指すCNAMEです。誰でも放置されたターゲットを登録でき、有効な証明書付きでold-app.yourdomain.com上にあなたの名前でコンテンツを配信できます。レコードの存在を誰も覚えていないため、バグバウンティではよく見つかる指摘です。
ドメインをサブドメイン検索にかけてください。証明書透明性ログの2つのサービス、Cert Spotter(現在の証明書)とcrt.sh(2013年以降のすべての証明書)を検索し、一般的な名前1,000個を試し、各名前をリアルタイムで名前解決して、ぶら下がったCNAMEを示し、それぞれを運用しているホストやCDNを表示します。ターゲットがNXDOMAINや、プロバイダーの「no such app」ページを返すレコードは削除してください。単一の名前の確認にはDNSルックアップが使えます。四半期ごとに繰り返し、サービスを廃止するときはDNSレコードの削除を手順に組み込みます。
証明書の透明性は早期警報システムにもなります。Cert SpotterやCloudflareのCTモニタリングを使えば、どこかのCAがあなたのドメインの証明書を発行するたびにメールで通知を受け取れます。身に覚えのない証明書は、DNSやレジストラが侵害されたことを示す最初の目に見える兆候であることが少なくありません。
レイヤー2:HTTPSとTLSを正しく設定する
HTTPSはもはやベストプラクティスではなく、前提です。ブラウザはHTTPのみのページに「保護されていない通信」と表示します。Googleはさらに、Chrome 154(2026年10月)から、公開サイトをHTTPのまま開く前にブラウザが確認を求めると発表しました(Google)。安全なサイトと単に暗号化されただけのサイトを分けるのは、受け入れるTLSのバージョン、HTTPで到達できる経路が残っていないか、そして2026年3月に始まった証明書の有効期間短縮に耐えられるほど更新が自動化されているか、の3点です。
TLS 1.2を最低ラインに、TLS 1.3を優先
TLS 1.0と1.1は2021年にRFC 8996で正式に非推奨となり、現行のブラウザはどれもこれらをネゴシエートしません。サーバーでは無効にし、TLS 1.3(既知の弱い暗号スイートをすべて排除し、1往復でハンドシェイクを完了します)を優先してください。TLS 1.2は、ECDHE-ECDSA-AES128-GCM-SHA256やECDHE-RSA-CHACHA20-POLY1305のようなAEAD暗号スイートに限って残します。
外部からテストするときに重要なのは2行です。ネゴシエートされたプロトコルと、証明書チェーン全体が検証されたことを示すVerify return code: 0です。中間証明書の欠落は、「Chromeでは動くのにcurlやAndroidでは失敗する」という報告のよくある原因です。SSL証明書チェーンのガイドで直し方を説明しており、SSLチェックはチェーンの不完全さを直接指摘します。以下は2026年10月27日にdnsrobot.netに対して実行した確認と、サーバーがTLS 1.1を拒否するかのテストです。
echo | openssl s_client -connect dnsrobot.net:443 -servername dnsrobot.net 2>/dev/null \
| grep -E 'Protocol|Cipher|Verify return'
# New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
# Protocol: TLSv1.3
# Verify return code: 0 (ok)
# Is TLS 1.1 refused? SECLEVEL=0 lets OpenSSL 3 and later offer it at all
openssl s_client -connect example.com:443 -servername example.com -tls1_1 \
-cipher 'DEFAULT:@SECLEVEL=0' </dev/null 2>&1 | grep -E 'alert|Cipher is'
# Refused, as it should be: ...tlsv1 alert protocol version...
# New, (NONE), Cipher is (NONE)
# Still accepted (fix it): New, TLSv1.0, Cipher is ECDHE-ECDSA-AES128-SHA
# (TLSv1.0 is the cipher's version; the handshake was TLS 1.1)コピー&ペーストした設定が古くなりやすいのが暗号スイートのリストです。手書きする代わりに、Mozilla SSL Configuration Generatorでnginx、Apache、Caddy、HAProxy向けの最新の推奨サーバー設定を生成しましょう。非常に古いクライアントをサポートする必要がない限り、「Intermediate」プロファイルを選んでください。
HSTS:ブラウザに二度とHTTPを使わせない
HTTPからHTTPSへのリダイレクトだけでは不十分です。訪問者がhttp://yourdomain.comに送る最初のリクエストは依然として平文で送られ、同じネットワーク上の攻撃者はリダイレクトより先に応答できてしまいます。これを解決するのがHTTP Strict Transport Security(HSTS)(RFC 6797)です。一度このヘッダーを受け取ったブラウザは、以後あなたのドメインへのhttp://のURLを、何かを送信する前にすべてhttps://に書き換えます。
max-ageは最低1年(31536000秒)にし、すべてのサブドメインがHTTPSを提供したらincludeSubDomainsを追加します。ドメインをChrome、Firefox、Safari、Edgeに組み込むhstspreload.orgでのプリロードは任意です。リストの管理者自身が、HSTSは推奨するがプリロードは推奨しないと述べています。ChromeとSafariはすでにHTTPSへ自動で引き上げており、リストから外れるには何か月もかかるためです。ヘッダーは、Mozilla HTTP Observatoryのテストの基準でHSTSを採点するHTTPヘッダーチェックで確認してください。
Strict-Transport-Security: max-age=31536000; includeSubDomains有効期間47日の証明書へ:今すぐ更新を自動化する
CA/Browser Forumの投票SC-081により、すべての公的TLS証明書の最大有効期間は段階的に短縮されています。最初の短縮は2026年3月15日に発効したため、今日購入・更新する証明書はすでに200日に制限されており、2029年にはおよそ6週間ごとに証明書を差し替える必要が出てきます。
| 発効日 | 証明書の最大有効期間 | ドメイン認証の再利用期間 |
|---|---|---|
| 2026年3月15日より前 | 398日 | 398日 |
| 2026年3月15日 | 200日 | 200日 |
| 2027年3月15日 | 100日 | 100日 |
| 2029年3月15日 | 47日 | 10日 |
手動の更新ではこのスケジュールに耐えられません。ベストプラクティスはACMEによる完全自動の発行です。certbot、acme.sh、Caddyを通じたLet's EncryptやGoogle Trust Services、あるいはCloudflare、Vercel、Netlify、多くのホスティングパネルに組み込まれた自動証明書を使いましょう。更新の経路は必要になる前にテストしてください(certbotならcertbot renew --dry-run)。注意すべき失敗は、前回の更新以降に追加されたCAAレコードやファイアウォールのルールが検証をブロックしているケースです。何を使うにしても、有効期限は外部からも監視しましょう。SSLチェッカーは残り日数を表示します。証明書が期限切れになると、すべての訪問が全画面のブラウザ警告に変わってしまいます。
レイヤー3:HTTPセキュリティヘッダー
セキュリティヘッダーは、ページに対してブラウザが何をしてよく、何をしてはいけないかをサーバーが指示するものです。どのスクリプトを実行できるか、ページをフレームに埋め込めるか、コンテンツタイプを推測してよいか、訪問者の遷移元を他のサイトにどう伝えるか、などです。費用はかからず、全ページに一度に適用でき、攻撃の種類をまるごと防げます。一方で、多くのサイトが一部を間違えるレイヤーでもあるため、HTTPヘッダーチェックではMozilla HTTP Observatoryのテストで採点します。以下は2026年10月27日にgithub.comが送信したヘッダーを、セキュリティ関連だけに絞ったものです(script-srcにはアセットホスト1つとスクリプトのハッシュだけが指定され、'unsafe-inline'はありません)。
curl -sI https://github.com/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy'
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
referrer-policy: origin-when-cross-origin, strict-origin-when-cross-origin
content-security-policy: default-src 'none'; base-uri 'self'; ... frame-ancestors 'none'; ... script-src github.githubassets.com 'sha256-6AKuI+cDd1MoUL1u4eqrA9PXmnPAkOB4h7421hPUUm0='; ...すべてのサイトが送るべき7つのヘッダー
一般的なサイトで目指すべき値です。当社のHTTPヘッダーチェックが使うMozilla HTTP Observatoryの採点では、CSPがないと25点、HSTSやフレーミング対策がないと各20点、nosniffがないと5点が引かれます。Referrer-PolicyとCOOPは、スコアが90以上のままの場合にボーナス点が加わります。Permissions-Policyは採点されません(読み取るのはChromium系ブラウザだけ)が、設定しておく価値はあります。
| ヘッダー | 推奨値 | 防げる攻撃 |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains | プロトコルのダウングレード、HTTP経由でのCookie窃取 |
Content-Security-Policy | nonceベースのscript-src('strict-dynamic'付き)、object-src 'none'、base-uri 'none'、frame-ancestors 'none' | クロスサイトスクリプティング(XSS)、データインジェクション、クリックジャッキング |
X-Content-Type-Options | nosniff | アップロードされたファイルを実行可能なスクリプトに変えてしまうMIMEスニッフィング |
Referrer-Policy | strict-origin-when-cross-origin | 完全なURL(トークンや検索語)の第三者への漏えい |
Permissions-Policy | camera=(), microphone=(), geolocation=(), payment=() | サードパーティのスクリプトによる強力なブラウザAPIの無断使用 |
X-Frame-Options | DENY(frame-ancestorsのレガシーな代替) | CSP Level 2に対応していないブラウザでのクリックジャッキング |
Cross-Origin-Opener-Policy | same-origin | window.openerを介したクロスウィンドウ攻撃 |
サイトを壊さずにContent Security Policyを導入する
Content Security Policy(CSP)は、インジェクションのバグが存在していても注入されたスクリプトの実行を止められるため、XSSに対して最も効果的な防御策です。難点は、厳格なポリシーを適用すると、忘れていたインラインスクリプトやサードパーティのタグがすべて動かなくなることです。Googleが推奨し、MDNでも解説されている現在のアプローチはnonceベースのポリシーです。サーバーがレスポンスごとにランダムなnonceを生成して、意図して出力するすべてのscriptタグに付与し、ブラウザはそれ以外のスクリプトをすべて拒否します。
このポリシーを実用的にしているのが'strict-dynamic'です。nonce付きのスクリプトは、個別に許可リストへ載せなくても追加のスクリプト(アナリティクス、タグマネージャー、ウィジェット)を読み込めます。一方、https:と'unsafe-inline'のトークンは最新のブラウザでは無視され、古いブラウザ向けのフォールバックとしてのみ機能します。まずContent-Security-Policy-Report-Onlyで展開し、1週間ほど違反レポートを確認してから強制モードに切り替えましょう。Next.js、Rails、Djangoなどのフレームワークは公式にnonceをサポートしています。静的サイトでは、代わりにハッシュベースのscript-srcを使えます。
Content-Security-Policy:
script-src 'nonce-r4nd0m1z3dV4lu3' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
report-to csp-endpoint
<!-- Only scripts carrying the nonce run; strict-dynamic lets them load their own dependencies -->
<script nonce="r4nd0m1z3dV4lu3" src="/app.js"></script>script-src 'self' https://cdn.example.comのような許可リスト型のポリシーは何もないよりはましですが、許可したホスト上のJSONPエンドポイントや古いライブラリを使って回避されるおそれがあります。まだnonceを使えない場合でも、少なくともobject-src 'none'とbase-uri 'none'は設定してください。よくある回避手法のうち2つをすぐに塞げます。
クリックジャッキング対策:frame-ancestorsとX-Frame-Options
クリックジャッキングは、攻撃者のページの中にあなたのサイトを見えない状態で読み込み、訪問者に見えないボタンをクリックさせる攻撃です。CSPのframe-ancestors 'none'(自サイトのページを埋め込む場合は'self')で現行のすべてのブラウザで防げ、X-Frame-Options: DENYで古いブラウザもカバーできます。埋め込み可能なウィジェットを意図的に提供する場合は、そのパスだけを、必要なオリジンに対してだけ除外してください。X-Frame-Optionsガイドでは、具体的なサーバーのルールと、テスト中に遭遇する「refused to connect」エラーを詳しく解説しています。
nosniff、Referrer-Policy、Permissions-Policy、COOP
X-Content-Type-Options: nosniffは、ブラウザがContent-Typeを勝手に推測し直すのを防ぎます。この推測こそが、HTMLを含むユーザーアップロードの「画像」を実行可能なページに変えてしまう原因です。Referrer-Policy: strict-origin-when-cross-origin(現行ブラウザのデフォルトですが、明示的に設定しましょう)は、ほかのサイトにはオリジンだけを送るため、URLに含まれるパスワードリセット用のトークンや検索クエリが外部に漏れません。Permissions-Policyは使っていないブラウザ機能を無効にするので、侵害されたサードパーティのスクリプトがカメラを起動したり位置情報を読み取ったりできなくなります。Cross-Origin-Opener-Policy: same-originは、開いたページとのwindow.openerのつながりを切断し、クロスウィンドウ攻撃の一群をブロックします。
削除すべきヘッダーも重要です。ServerとX-Powered-Byは正確なソフトウェアのバージョンを脆弱性スキャナーに知らせてしまい、X-XSS-Protectionは非推奨で古いブラウザーで不具合を招く可能性があるため、0にするか削除してください。CMS検出ツールを使うと、ページ自体から誰でも把握できるスタックとそのバージョンを確認できます。
ヘッダーはページごとではなく、エッジ(CDN、リバースプロキシ、フレームワークのミドルウェア)で一度だけ設定しましょう。Next.jsならproxyまたはmiddlewareのファイル、nginxならserverコンテキストのadd_headerブロック、ApacheならHeader always setです。そしてデプロイのたびにHTTPヘッダーチェッカーを再実行してください。フレームワークの新バージョンやCDNの設定変更で、ヘッダーが1つ黙って消えることがあるためです。
レイヤー4:クレデンシャルスタッフィングに耐える認証
攻撃者がパスワードを1つずつ推測することはほとんどありません。ほかのサイトから流出した何十億ものメールアドレスとパスワードの組み合わせを使い回し(クレデンシャルスタッフィング)、残りはフィッシングで盗みます。そのため認証のベストプラクティスは3つの要素からなります。データベースが流出してもパスワードは流出しないように保存すること、盗まれたパスワード単体では役に立たないようにすること、そしてブルートフォースを意味がないほど遅くすることです。OWASP Top 10:2025では、Authentication Failures(認証の失敗)がA07に位置づけられています。
パスワードはArgon2idまたはbcryptでハッシュ化する
パスワードをそのまま保存してはいけませんし、高速なハッシュで保存してもいけません。MD5、SHA-1、さらにはSHA-256も高速に計算できるよう設計されているため、流出したハッシュの一覧は1枚のGPUで毎秒数十億回の推測にかけられます。パスワードごとに固有のソルトを付けたうえで、低速でメモリ負荷の高いパスワードハッシュ関数を使いましょう。OWASP Password Storage Cheat Sheetは、次の順で推奨しています。
- Argon2id:メモリ最低19 MiB、反復回数2、並列度1(または46 MiBで反復回数1)。
- scrypt:Argon2が使えない場合に、N = 2^17、r = 8、p = 1で使用。
- bcrypt:ワークファクター10以上(入力が72バイトまでという制限に注意。長いパスワードを事前ハッシュする場合は慎重な実装が必要です)。
- PBKDF2-HMAC-SHA256:FIPS準拠が必須の場合に限り、反復回数600,000回で使用。
// Node.js with the argon2 package
import argon2 from "argon2";
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // KiB = 19 MiB
timeCost: 2,
parallelism: 1,
});
// Later, on login:
const ok = await argon2.verify(hash, submittedPassword);ログインサーバーでハッシュ1回に約100〜250ミリ秒かかるようにコストを調整してください。実際のユーザーには気になりませんが、CPUコア1つあたりの試行回数は毎秒わずかに、ハイエンドのグラフィックスカードでも数千回に抑えられ、数十億回にはなりません。パラメーターを引き上げたときは、ログイン成功時に再ハッシュして、古いハッシュが自動的に更新されるようにします。
本当に役立つパスワードルール(NIST SP 800-63B)
多くのサイトがいまだに強制している文字種の組み合わせルール(大文字を1つ、記号を1つ、90日ごとに変更)は、Summer2026!のような予測しやすいパスワードを生み、使い回しを促してしまいます。現在のNIST SP 800-63Bのガイダンスは、これらを本当に重要な点を測るルールに置き換えています。
- MFAも必須にする場合は最低8文字、パスワード単体で使う場合は最低15文字とし、少なくとも64文字まで入力できるようにします。
- 文字種の組み合わせルールは設けず、定期的な変更も強制しません。変更を求めるのは、侵害の証拠がある場合だけです。
- 新しいパスワードはすべて流出リストと照合し(Have I Been Pwnedのrange APIを使えば、パスワード自体を送信せずに照合できます)、既知の流出パスワードは拒否します。
- 貼り付けとパスワードマネージャーを許可し、スペースとUnicodeを受け入れ、強いパスワードを選ぶ助けになるガイダンスを示します。特に、パスワードを拒否した直後が重要です。
パスワード強度テストを使うと、これらの対策が従来のルールとどう違うかがわかります。名前の付いた8種類の攻撃でパスワードの解読にかかる時間を見積もり、強度を下げるパターンを示し、パスワードを送信せずに漏えいデータと照合します。赤字の「記号が必要です」よりはるかに役立つフィードバックです。
MFAとパスキー
多要素認証を導入すれば、盗まれたパスワードは行き止まりになります。すべてのユーザーに提供し、管理者、経理、サポートの役割では必須にしましょう。選択肢はフィッシング耐性で順位づけします。パスキーとFIDO2セキュリティキーは、認証情報が正規のオリジンに結びついているためフィッシングされません。認証アプリ(TOTP)は良い選択肢です。SMSコードはないよりはましですが、SIMスワップによって傍受されるおそれがあります。パスキーは現行のすべてのブラウザとOSでサポートされており、パスワードそのものをなくせるため、クレデンシャルスタッフィングという攻撃カテゴリー自体も消えます。
アカウント復旧の経路もログインと同じくらい慎重に守りましょう。リカバリーコードはハッシュ化して保存し、メールによるリセットには15分以内に失効する使い捨てトークンを使い、秘密の質問は使わないようにします。
レート制限、アカウントロック、ボット対策
ログイン、登録、パスワードリセット、MFAの各エンドポイントには、IPごとかつアカウントごとにレート制限をかけ、上限に達したら429 Too Many RequestsをRetry-Afterヘッダー付きで返します(クライアント側の対応はHTTPエラー429のガイドで解説しています)。これに加えて、1つのアカウントで失敗が続いた場合は段階的に遅延を増やすか一時的にロックし、分散型の攻撃を受けるエンドポイントにはCAPTCHAやプルーフ・オブ・ワークのチャレンジを導入します。失敗したログインはすべて送信元IPとともに記録しておけば、クレデンシャルスタッフィングのパターンを数か月後ではなく数分で見つけられます。
# nginx: 5 login attempts at once, then 1 every 12 seconds, per client IP
# (limit_req sends no Retry-After header; add one on your 429 page if clients need it)
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location = /api/login {
limit_req zone=login burst=4 nodelay;
limit_req_status 429;
proxy_pass http://app;
}「ユーザーが存在しない」場合と「パスワードが違う」場合でエラーメッセージを同一にし、応答時間も揃えてください(ユーザーが存在しないときはダミーのパスワードをハッシュ化します)。そうしないと、ログインフォームがユーザー列挙用のAPIとして使えてしまいます。
セッション、Cookie、CSRF
ログイン後は、セッションCookieがユーザーそのものです。したがって、パスワードと同等の保護が必要です。3つのCookie属性と1つの名前プレフィックスで、対策のほとんどをまかなえます。
Secureを付けると、Cookieは平文のHTTPで送信されなくなるため、公衆Wi-Fiで盗聴されません。HttpOnlyを付けるとJavaScriptから見えなくなるため、XSSのバグがあってもCookieを読み取られません。SameSite=Lax(管理画面ならStrict)を付けると、クロスサイトのPOSTリクエストでCookieが送られなくなり、ほとんどのCSRF攻撃を無力化できます。__Host-プレフィックスを付けると、Secureがあり、Path=/で、Domain属性がない場合を除いてブラウザがCookieを拒否するため、サブドメインから上書きされなくなります。
Set-Cookie: __Host-session=9f1c7e2a...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=28800状態を変更する操作は、決してGETで実行できるようにしてはいけません。/account/delete?id=42のようなリンクは、ユーザーが訪れた任意のページ上の<img>タグから実行できてしまいます。Cookieのフラグがどれほど適切でも同じです。
CSRF:SameSiteの背後にもう1つの防御線を
クロスサイトリクエストフォージェリ(CSRF)は、ログイン中のブラウザをだまし、別のページからあなたのサイトへ状態を変更するリクエストを送信させる攻撃です。SameSite Cookieで一般的なケースは防げますが、GET以外のすべてのリクエストにはもう1つ防御を残しておきましょう。hiddenフィールドやカスタムヘッダーに入れるセッションごとのCSRF対策トークンか、Origin(またはSec-Fetch-Site)ヘッダーが自サイトのオリジンと一致するかのチェックです。ほとんどのフレームワーク(Django、Rails、Laravel、Next.jsのServer Actions)はデフォルトでこれを行います。よくある失敗は、APIエンドポイントでこれを無効にしたうえで、そのエンドポイントをブラウザから呼び出すことです。
セッションID、ローテーション、JWT
セッションIDは少なくとも128ビットのランダム性をもって生成し、セッション固定攻撃を防ぐためにログイン時と権限が変わるたびにIDをローテーションし、一定時間操作がなければセッションを失効させ、サーバー側でセッションを無効化する「すべてのデバイスからログアウト」ボタンを用意しましょう。JWTは有効期間を短くし(数分程度にして、失効させられるリフレッシュトークンと組み合わせます)、どのスクリプトからも読めるlocalStorageには決して保存しないでください。署名鍵が漏れたら完全な侵害として扱います。その鍵でこれまでに署名されたすべてのトークンが偽造可能になるからです。
インジェクションとXSS:入力を検証し、出力をエンコードする
インジェクション(Injection、A05:2025)とクロスサイトスクリプティングは、場所が違うだけで同じ間違いです。ユーザーからのデータを、まるでコードであるかのようにインタープリター(SQL、シェル、LDAPクエリ、HTMLページ)に渡してしまうことです。対策もどこでも同じで、データとコードを分離し、文字列の連結でコマンドを組み立てないことです。
文字列を組み立てず、パラメータ化クエリを使う
SQLでは、プリペアドステートメントか、それを生成するORMを使いましょう。そうすればデータベースは入力を値として扱い、クエリの構造が変わることはありません。同じルールはシェルコマンド(文字列ではなく引数の配列で渡す)、NoSQLのフィルター(ユーザー入力に含まれる{"$gt": ""}のような演算子オブジェクトを拒否する)、ファイルパス(パスを解決し、想定したディレクトリの配下に収まっていることを確認する)にも当てはまります。
// Vulnerable: the input becomes part of the query text
const rows = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// Safe: the driver sends the value separately from the query
const rows = await db.query("SELECT * FROM users WHERE email = $1", [email]);
// Python / psycopg: same idea
cur.execute("SELECT * FROM users WHERE email = %s", (email,))出力エンコードでXSSを防ぐ
クロスサイトスクリプティングは、ユーザーが制御できるテキストを、出力先のコンテキストに合わせてエンコードせずにページへ書き込んだときに発生します。最近のテンプレートエンジン(ReactとJSX、Vue、Jinja2、Blade、ERB)はデフォルトでエンコードするため、現在のXSSの多くは抜け道から生まれます。dangerouslySetInnerHTML、v-html、|safe、innerHTML、そしてユーザーデータからURLやインラインのイベントハンドラーを組み立てる処理です。出力先のコンテキストに正確に合わせてエンコードし、リッチなHTMLは正規表現ではなくDOMPurifyのような専用ライブラリでサニタイズしましょう。
| データの出力先 | エンコード方法 | 例 |
|---|---|---|
| HTML本文 | HTMLエンティティ | <は<に、"は"に変換 |
| HTML属性 | 属性値を引用符で囲んだうえでのエンティティ化 | 属性値は必ず引用符で囲み、"と'をエンコード |
| JavaScript | スクリプトのテキストに直接埋め込まない。data-属性かJSONの<script type="application/json">ブロック経由でデータを渡す | 文字列内の</script>は\u003c/script\u003eに変換 |
| URLパラメータ | パーセントエンコーディング | encodeURIComponent(value) |
| CSSの値 | 一切使わない。クラス名を許可リストから選ぶ | ユーザー入力をstyleに埋め込まない |
SSRF、ファイルアップロード、デシリアライズ
サーバーサイドリクエストフォージェリ(SSRF):サーバーがユーザーから指定されたURLを取得する場合(Webhook、画像プロキシ、リンクプレビューなど)、攻撃者はその宛先を内部サービスやクラウドのメタデータエンドポイント169.254.169.254に向け、認証情報を読み取れます。まずホスト名を名前解決し、プライベートおよびリンクローカルの範囲を拒否し、リダイレクトを無効にし、取得処理は内部の何にも到達できないネットワークセグメントに置きましょう。
ファイルアップロード:ファイルの種類は、拡張子やクライアントが送るContent-Typeではなく、中身を調べて検証します。サイズの上限を設け、ファイルはWebルートの外かオブジェクトストレージに、自分で生成したランダムな名前で保存し、可能であればnosniffとContent-Disposition: attachmentを付けて別のオリジンから配信します。アップロードされたファイルが、Webサーバーに実行される場所に置かれることがあってはなりません。
デシリアライズとマスアサインメント:信頼できないデータを、任意のクラスをインスタンス化できる形式(Javaのネイティブシリアライズ、Pythonのpickle、PHPのunserialize、カスタムタグ付きのYAML)でデシリアライズしてはいけません。スキーマ付きのJSONを使いましょう。リクエストボディは明示的な許可リストのフィールドにだけバインドし、たまたまそのカラムを持つモデルにユーザーが"role": "admin"をPOSTできないようにします。
アクセス制御の不備(Broken Access Control):第1位のリスク
Broken Access Control(アクセス制御の不備)は2021年からOWASP Top 10の首位を守り続け、A01:2025でもその座にあります。これは単一のバグではなく、習慣の問題です。リクエストのたびに、相手が誰か(認証)は確認しても、その人が何に触れてよいか(認可)の確認を忘れてしまうのです。典型的な形が安全でない直接オブジェクト参照(IDOR)です。GET /api/invoices/1042は請求書の所有者には機能しますが、番号を書き換えた誰にでも機能してしまいます。
- デフォルトで拒否します。 すべてのルートでアクセスを許可する明示的なルールを必須にし、ルールがなければ200ではなく403を返します。
- サーバー側で、オブジェクトごとに強制します。 UIでボタンを隠すのはアクセス制御ではありません。一括処理のエンドポイント、エクスポート、バックグラウンドジョブも含め、すべての読み書きで、現在のユーザーがその特定のレコードを所有しているか、アクセスを許可されているかを確認します。
- 役立つ場面では推測困難なID(UUID)を使いますが、それに頼ってはいけません。隠すことは認可ではありません。
- CORSを厳格にします。 認証情報付きの
Access-Control-Allow-Origin: *や、リクエストが送ってきたOriginをそのまま返す設定は、ユーザーが訪れるあらゆるWebサイトにAPIを明け渡すことになります。 - ディレクトリ一覧表示を無効にし、
.git、.env、バックアップファイル、設定ファイルへのアクセスをWebサーバーのレベルでブロックします。管理画面は公開インターネットから切り離すか、MFAとIP許可リストで保護します。 - 認可の失敗にもレート制限をかけ、ログに記録します。 1つのセッションから403が連続するのは、列挙攻撃が進行中のサインです。
// Express: ownership check on every object access
app.get("/api/invoices/:id", requireAuth, async (req, res) => {
const invoice = await Invoice.findById(req.params.id);
if (!invoice || invoice.ownerId !== req.user.id) {
return res.status(404).end(); // 404, not 403: don't confirm the record exists
}
res.json(invoice);
});レイヤー5:依存関係とソフトウェアサプライチェーン
典型的なWebアプリケーションのコードの大半は、自分で書いたものではなくダウンロードしたものです。そのためソフトウェアサプライチェーンの不備が2025年版OWASP Top 10でA03に初登場し、2026年版DBIRでは脆弱性の悪用がフィッシングや盗まれた認証情報を上回りました。2025年9月には、フィッシングに遭ったメンテナー1人が、chalk、debugなど合計で週20億回以上ダウンロードされる18個のnpmパッケージに暗号資産を盗むマルウェアを仕込みました。その間にバージョンを固定せずにインストールしたプロジェクトは、すべて自動的に取り込んでしまいました。
- ロックファイルをコミットし(
package-lock.json、yarn.lock、pnpm-lock.yaml、poetry.lock、go.sum)、CIではnpm ciでインストールします。これでビルドの再現性が保たれ、上流の新しいリリースがレビューなしに紛れ込むことを防げます。 - 継続的にスキャンします:
npm audit、pip-audit、bundler-audit、更新にはGitHub DependabotやRenovate、OSレイヤーにはTrivyやGrypeのようなコンテナスキャナーを使います。 - セキュリティ以外のアップグレードは数日遅らせます(Renovateの
minimumReleaseAge)。そうすれば、悪意あるリリースは多くの場合、取り込む前に取り下げられます。ただし、セキュリティパッチはすぐに適用してください。 - CIではサードパーティのアクションやスクリプトをコミットSHAに固定し、公開CDNから読み込むスクリプトにはすべてSubresource Integrity(
integrity="sha384-...")を付けます。 - CIには最小権限を与えます:可能な限り読み取り専用のトークンを使い、長期間有効なシークレットの代わりに短命のOIDC認証情報を使い、公開(publish)の権限はリリース用のジョブだけに限定します。
- SBOMを生成します(CycloneDXまたはSPDX)。ビルド時に生成しておけば、次にLog4Shell級のCVEが公表されたとき、「自分たちは影響を受けるのか」に数分で答えられます。
# Reproducible install + audit in CI
npm ci --ignore-scripts
npm audit --audit-level=high
# Pin a GitHub Action to a commit, not a floating tag
# uses: actions/checkout@v4 <- can change underneath you
# uses: actions/checkout@<full-commit-sha> # v4.2.2
# Subresource Integrity for a CDN script (hash from: openssl dgst -sha384 -binary lib.min.js | openssl base64 -A)
<script src="https://cdn.example.com/lib.min.js"
integrity="sha384-<base64-hash>"
crossorigin="anonymous"></script>npm ci --ignore-scriptsを使うと、ほとんどのnpmマルウェアが実行に使うインストール時のスクリプトをブロックできます。そのうえで、本当にビルド手順が必要な一部のパッケージに限ってスクリプトを再び有効にしましょう。
WordPressなどのCMSを運用している場合も、同じルールがプラグインとテーマに当てはまります。CMSの侵害の多くはそこから始まるからです。コアとすべてのプラグインを最新に保ち、使っていないものは削除し、スキャナーがバージョンについて何を把握できるかをCMS検出ツールで確認しましょう。
レイヤー6:サーバーのハードニング(ポート、パッチ、シークレット、バックアップ)
その下のサーバーがSSHでパスワードログインを受け付けていたり、データベースを公開ポートで動かしていたり、構築以来一度もパッチを当てていなかったりすれば、アプリケーション層の対策は意味をなしません。サーバーのベストプラクティスは地味ですが、ランサムウェアが侵入してくるのはまさにここです。
提供していないポートはすべて閉じる
公開Webサーバーで開けておく必要があるのは、80番と443番のポート、そして自社のIP範囲からのSSHだけです。データベース(3306、5432、27017)、Redis(6379)、Elasticsearch(9200)、管理画面、メトリクスのエンドポイントは、localhostかプライベートネットワークにのみバインドしましょう。変更のたびにポートチェッカーで外部から確認してください。それが攻撃者から見える姿であり、クラウドのセキュリティグループは読み違えやすいからです。
# Ubuntu: default-deny firewall, allow only web + SSH from your office range
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow from 203.0.113.0/24 to any port 22 proto tcp
sudo ufw enable # check your own IP is in that range first, or you lock yourself out
# SSH: keys only, no root password login, in a drop-in that sshd reads first
printf 'PasswordAuthentication no\nPermitRootLogin prohibit-password\n' \
| sudo tee /etc/ssh/sshd_config.d/00-hardening.conf
sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin) ' # what sshd will really use
sudo systemctl reload sshUbuntuとDebianでは、sshdはsshd_configの残りより先に/etc/ssh/sshd_config.d/内のファイルを読み、各設定では最初に読んだ値が優先されます。そのため上のコマンドは00-hardening.confというドロップインを書き込みます。メインファイルの下のほうで編集しても、PasswordAuthentication yesと書かれたクラウドイメージのドロップインに負けてしまうからです。ログアウトする前にsudo sshd -Tで結果を確認してください。ポートチェッカーでは、ファイアウォールが破棄するポートはfiltered(応答なし)、待ち受けがないポートはclosedと表示されます。どちらも問題なく、直すべきはopenです。
パッチは定期的に適用し、シークレットはコードに入れない
OSのセキュリティ更新を自動適用するよう設定し、ランタイムとフレームワークのセキュリティメーリングリストを購読し、インターネットに公開しているものの重大なCVEは当日中に対応すべき仕事として扱いましょう。サービスは非特権ユーザーとして、コンテナ内またはsystemdのサンドボックス機能を使って実行し、侵害されたプロセスがマシン全体を読み取れないようにします。
シークレット(データベースのパスワード、APIキー、署名鍵)は、リポジトリにも、Dockerイメージのレイヤーにも、クライアント側のJavaScriptにも置いてはいけません。デプロイ時に注入される環境変数かシークレットマネージャーから読み込み、アクセス権を持つ人が退職したらローテーションし、gitleaksのようなツールでリポジトリの履歴をスキャンしてください。一度コミットされた鍵は、コミットを削除したあとでも永久に漏えいしたものと見なすべきだからです。
ランサムウェアに耐えるバックアップと、前段のCDN
3-2-1ルールに従いましょう。コピーを3つ、2種類の異なるメディアに、1つはオフサイトに保管し、少なくとも1つはイミュータブル(変更不可)またはオフラインにして、サーバーに到達したランサムウェアがバックアップまで暗号化できないようにします。バックアップは暗号化し、データベースとアップロードディレクトリを含め、そして何より、定期的にリストアをテストしてください。誰も復元を試したことのないバックアップは、計画ではなく願望にすぎません。インシデントが一時的な障害で済むか、会社の存続にかかわる事態になるかを決めるのは復旧時間です。
オリジンの前にCDNまたはWAF(Cloudflare、Fastly、WAF付きのAWS CloudFront、またはホスティング会社の同等サービス)を置きます。大量のDDoSを吸収し、一般的なインジェクションのペイロードや悪質なボットに対するマネージドルールを適用し、オリジンIPを隠し、アプリケーションを変更せずにレート制限や地域ルールを適用する場所を提供します。そのうえでオリジンのファイアウォールを制限し、CDNのIPレンジだけがポート443に直接到達できるようにします。そうしないと、攻撃者はCDNを迂回するだけです。ホスティングチェッカーでは、サイトの前段にあるCDNと、その背後のホストについて公開情報が示す内容を確認できます。
メール認証:SPF、DKIM、DMARC
Webセキュリティには、パスワードリセット、請求書、サポート返信を運ぶメールも含まれます。SPF、DKIM、DMARCがなければ、誰でもbilling@yourdomain.comとしてメールを送信できます。2024年2月からGoogleとYahooが大量送信者にこの3つすべてを求めており、MicrosoftもOutlook.comで2025年5月から同じ要件を課しています。完全なセットは、3つのDNS TXTレコードです。
yourdomain.com. TXT "v=spf1 include:_spf.google.com -all"
google._domainkey.yourdomain.com. TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkq..."
_dmarc.yourdomain.com. TXT "v=DMARC1; p=reject; rua=mailto:dmarc@yourdomain.com; adkim=s; aspf=s"DMARCのp=rejectはブランド保護の仕組みでもあります。フィッシングキャンペーンが差出人(From)欄にあなたのドメインそのものを入れるのを止められるのは、これだけです。集計レポート(rua=)を見れば、誰がなりすましを試みているかがわかります。
DMARCはまずp=noneでレポートを集め、すべての正規の送信元(マーケティングプラットフォーム、ヘルプデスク、CRM)がアラインメントを通過したらp=quarantine、次にp=rejectへ進めます。MTA-STSとTLS-RPTのレコードを追加して、メールサーバーへの暗号化配信を強制します。メールを送受信しないドメインでは、ヌルMX(MX 0 .)、v=spf1 -all、p=rejectのDMARCレコードを公開してください。ヌルMXは「このドメインはメールを受け取らない」と示すだけで、受信側が偽装を拒否できるのはSPFとDMARCがあるからです。各レコードはSPFチェッカー、DKIMチェッカー、DMARCチェッカーで確認し、到達率が下がったらIPブラックリストチェッカーで送信元IPがブロックリストに載っていないか調べます。
ログを取り、監視し、侵害に備える
OWASP 2025の10カテゴリーのうち2つは、ミスが起きたあとの対応に関するものです。Security Logging and Alerting Failures(セキュリティログとアラートの失敗、A09)と、新たに加わったMishandling of Exceptional Conditions(例外的な状況の不適切な処理、A10)です。典型的な侵害がいまだに何か月も発見されないのは、証拠がそもそも記録されていないか、記録されていても誰も見ていないからです。
- セキュリティイベントを文脈付きで記録します:ログインの成功と失敗、MFAの変更、パスワードリセット、権限の変更、アクセス制御による拒否、入力検証の失敗、管理者の操作をすべて、タイムスタンプ、ユーザーID、送信元IP、ユーザーエージェントとともに記録します。
- シークレットは決してログに残しません:パスワード、セッショントークン、カード番号全体、APIキーなどです。呼び出し箇所ごとではなく、ロギング層でマスクしましょう。
- ログはサーバーの外に送ります:集中管理用のストア(CloudWatch、Loki、Elastic、SIEM)に少なくとも90日間保持し、root権限を得た攻撃者でも痕跡を消せないようにします。
- 単発のイベントではなくパターンでアラートを出します:1分間に50回のログイン失敗、1つのセッションからの403の急増、営業時間外に作成された新しい管理者、自分で申請していないドメインの証明書の発行などです。
- フェイルクローズにします:下流のチェック(認証サービス、レート制限、WAF)がエラーになったら、リクエストを拒否します。A10が存在するのは、本来ブロックするはずのチェックが例外を投げたときにアクセスを許可してしまうシステムが非常に多いからです。
- 外部から監視する:稼働状況、証明書の有効期限、DNSの変更、ブロックリストへの掲載、ヘッダーの退行を監視します。デプロイのたびに、SSLチェック、HTTPヘッダーチェック、DNSヘルスチェック(委任、ネームサーバー、DNSSEC、SPF/DMARCに関する40項目の確認)を再実行してください。
インシデント対応計画は必要になる前に書く
誰が待機するか、すべての認証情報をどうローテーションするか、サイトを読み取り専用にする方法、クリーンなバックアップの場所、どの規制当局や顧客に何時間以内に通知しなければならないか(GDPRでは72時間)を事前に決めておきます。年に1回は机上演習を行ってください。訓練済みの計画があれば、何日もの場当たり的な対応がチェックリストに変わります。
/.well-known/security.txtにsecurity.txtファイル(RFC 9116)を公開し、必須の2項目であるContact:行とExpires:の日付を記載してください(Encryption:の鍵は任意です)。サイトの不具合を見つけた研究者はまずそこを見ます。なければ、報告先が見つからないかもしれません。
テストする:スキャナー、ペネトレーションテスト、継続的なチェック
ここまでの対策はすべて検証でき、その検証は形骸化しないよう可能な限り自動化すべきです。無料ですぐにできるものから、定期的に行う有料のものまで、現実的な段階は次のとおりです。
| チェック項目 | ツール | 頻度 |
|---|---|---|
| TLSの設定、チェーン、有効期限 | SSLチェッカー、SSL Labs、testssl.sh | 証明書やサーバーを変更するたび。有効期限は毎日 |
| セキュリティヘッダーとCSP | HTTPヘッダーチェッカー、MDN HTTP Observatory | デプロイのたび(CIに組み込む) |
| 開いているポート | ポートチェッカー、nmap(自分のネットワークの外から実行) | ファイアウォールやクラウドの設定を変更するたび |
| DNS、DNSSEC、メール認証 | DNSヘルスチェック、DMARCチェッカー | 毎月、およびDNSを編集するたび |
| 放置されたサブドメイン | サブドメイン検索 | 四半期ごと、およびサービスを廃止するたび |
| 既知の脆弱性がある依存関係 | npm audit、Dependabot、Trivy | ビルドのたび |
| コードレベルの欠陥(SAST) | Semgrep、CodeQL | プルリクエストのたび |
| 実行時のWeb脆弱性(DAST) | ZAP、Nuclei | ステージング環境に対して毎週 |
| 認可とビジネスロジック | 手動のペネトレーションテストまたはバグバウンティプログラム | 年1回、および大きな機能の追加後 |
スキャナーはインジェクション、ヘッダー、古いコンポーネントの検出は得意ですが、認可やビジネスロジックは苦手です。お金や個人データを扱うものについては、少なくとも年1回は人による検査の予算を確保しましょう。修正は露出度の高い順に行います。インターネットから認証なしで悪用できるものが最優先です。
Webセキュリティ対策チェックリスト
これをプロジェクトのREADMEやチケット管理ツールにコピーしてください。各行は上で紹介した対策の1つで、完了か未完了かを判断できる形で書いています。最後の列がその証拠です。
| 層 | 対策 | 完了の基準 |
|---|---|---|
| ドメイン | レジストラロックとレジストラアカウントのMFA、自動更新をオン | WHOISにclientTransferProhibitedが表示され、有効期限が12か月以上先 |
| DNS | DNSSECで署名済み、自分のCAだけを記載したCAAレコード | dig +dnssecがRRSIGを返し、親ゾーンにDSレコードがある |
| DNS | 放置されたCNAMEレコードやAレコードがない | サブドメイン検索で見つかったすべてのホスト名が、自分で管理しているものに解決される |
| TLS | TLS 1.2以上のみ、TLS 1.3を優先、完全なチェーンを配信 | SSLチェッカーでチェーンが完全、TLS 1.0/1.1が拒否される |
| TLS | 1年のmax-ageとincludeSubDomainsを指定したHSTS(プリロードは任意) | HTTPヘッダーチェックでHSTSのテストに合格 |
| TLS | ACMEで更新を自動化、有効期限を監視 | certbot renew --dry-runが成功し、期限14日前のアラートを設定済み |
| ヘッダー | CSP(nonceベース)、frame-ancestors、nosniff、Referrer-Policy、Permissions-Policy | HTTPヘッダーチェックの評価がAまたはA+ |
| 認証 | Argon2idまたはbcryptでのハッシュ化、NISTのパスワードルール、流出リストとの照合 | ハッシュ1回に100〜250ミリ秒かかり、文字種の組み合わせルールがない |
| 認証 | MFAを全員に提供し管理者には必須、パスキーに対応 | 第2要素なしでは管理者としてログインできない |
| 認証 | ログイン、リセット、MFAのエンドポイントにIPごと・アカウントごとのレート制限 | 1つのIPから6回続けて試行し、6回目が429を返す |
| セッション | Secure・HttpOnly・SameSite付きの__Host- Cookie、ログイン時にIDをローテーション | DevToolsでCookie属性が確認でき、ログイン後は古いセッションが無効になる |
| 入力 | すべてのクエリをパラメータ化、コンテキストごとに出力をエンコード、アップロードは中身で検証 | コード検索で文字列連結のクエリが見つからず、XSSのペイロードがテキストとして表示される |
| アクセス制御 | デフォルト拒否のルーティング、オブジェクトごとの所有者チェック、厳格なCORS | 別のユーザーのIDで再送すると404または403が返る |
| 依存関係 | ロックファイルをコミット、npm ci、CIで監査、アクションをSHAで固定、CDNスクリプトにSRI | 深刻度の高いCVEがあるとビルドが失敗する |
| サーバー | 公開は80番と443番のみ、SSHは鍵認証のみ、セキュリティ更新の自動適用、シークレットは環境変数かVaultに保管 | ポートチェッカーで3306と6379がclosedまたはfilteredと表示され、22も自分の範囲外からはそうなる |
| バックアップ | イミュータブルなコピー1つを含む3-2-1、リストアをテスト済み | 最後に成功したリストアテストが90日以内 |
| メール | SPFは-all、DKIM、DMARCはp=reject、MTA-STS | DMARCチェッカーで合格し、集計レポートが届いている |
| 監視 | 認証・認可のイベントを集中的に記録、パターンでアラート、稼働状況・証明書・DNSを外部から監視 | テスト用のアラートが発報され、受信できた |
| 対応 | インシデント対応計画を文書化、security.txtを公開、机上演習を実施 | 直近12か月以内に計画を見直している |
OWASP Top 10:2025との対応表
OWASP Top 10は、webアプリケーションのリスクを示すリストとして最も多く引用されており、2025年版では順位が入れ替わり、2つの新しいカテゴリーが加わりました。顧客や監査人、コンプライアンスのフレームワークからどう対処しているかを聞かれた場合は、このガイドの対策との対応表として次を使ってください。
| OWASP Top 10:2025 | このガイドでの対策 |
|---|---|
| A01 Broken Access Control(アクセス制御の不備) | デフォルト拒否、オブジェクトごとのチェック、厳格なCORS、管理画面の保護 |
| A02 Security Misconfiguration(セキュリティ設定のミス) | セキュリティヘッダー、HSTS、ポートの閉鎖、バージョンヘッダーの削除、ディレクトリ一覧表示の無効化 |
| A03 Software Supply Chain Failures(ソフトウェアサプライチェーンの失敗、新規) | ロックファイル、監査、アクションの固定、SRI、SBOM、最小権限のCI |
| A04 Cryptographic Failures(暗号化の失敗) | TLS 1.2以上と1.3、HSTS、Argon2id、シークレット管理、バックアップの暗号化 |
| A05 Injection(インジェクション) | パラメータ化クエリ、出力エンコード、CSP、アップロードの検証 |
| A06 Insecure Design(安全でない設計) | 層ごとの脅威モデル、フェイルクローズのデフォルト設定、設計段階からのレート制限 |
| A07 Authentication Failures(認証の失敗) | NISTのパスワードルール、流出リストとの照合、MFAとパスキー、セッションのローテーション |
| A08 Software or Data Integrity Failures(ソフトウェアまたはデータの整合性の失敗) | SRI、署名・固定された依存関係、安全なデシリアライズ |
| A09 Security Logging and Alerting Failures(セキュリティログとアラートの失敗) | ログの集中管理、パターンによるアラート、外部からの監視 |
| A10 Mishandling of Exceptional Conditions(例外的な状況の不適切な処理、新規) | フェイルクローズ、統一されたエラー応答、ユーザーにスタックトレースを見せない |
OWASPのASVS(Application Security Verification Standard)は、同じ内容を監査用の番号付きチェックリストにまとめたものです。公開サイトならレベル1が現実的な目標で、個人データや金融データを扱うものならレベル2を目指しましょう。
サイトのセキュリティヘッダーを数秒でチェック
DNS Robotの無料HTTPヘッダーチェッカーは、任意のURLのレスポンスヘッダーをサーバーが送ったとおりに表示し、Mozilla HTTP Observatoryのテストでセキュリティヘッダーを採点して、不足しているものごとに対処法を示します。登録は不要です。
出典
- 2026 Data Breach Investigations Report, Verizon
- OWASP Top 10:2025, OWASP
- Password Storage Cheat Sheet, OWASP
- SP 800-63B-4: Authentication and Authenticator Management, NIST
- Ballot SC-081v3: reducing certificate validity and data reuse periods, CA/Browser Forum
- HTTPS by default, Google
- RFC 9116: A File Format to Aid in Security Vulnerability Disclosure, IETF



