はじめに
「脆弱性(セキュリティホール)」とは、ソフトウェアや設定に潜む攻撃者に悪用されうる弱点のことです。やっかいなのは、脆弱性があっても正常な使い方ではちゃんと動いてしまう点です。動作テストは「正しく使えば正しく動くか」を確認するもので、「悪意を持って想定外の使い方をされたらどうなるか」までは見ていないことが多い。脆弱性対策とは、この“悪意ある使われ方”への備えにほかなりません。
この記事は、Web開発・運用に携わるエンジニア/情報システム担当向けに、OWASP Top 10(2021年版)を軸にして「どこを・どう守るか」を、実務でそのまま使えるコード・設定例とつまずきどころ付きで整理したものです。網羅解説ではなく、手を動かす起点を提供することを目的にしています。
補足:まだ修正パッチが提供されていない脆弱性を突く攻撃を「ゼロデイ攻撃」と呼びます。
脆弱性が生まれる4つの定番要因
特殊な原因より、身近な“油断”から生まれることがほとんどです。
- 入力をそのまま信用する → インジェクションの温床。外部入力は疑う。
- 設定が緩いまま → 初期パスワード・公開範囲・詳細エラー表示の放置。
- 古いまま放置 → 既知の弱点を抱えたまま、公開済みの攻撃手法で狙われる。
- 認証・権限が甘い → MFAなし・過剰な権限付与。
以降は、これらが OWASP Top 10 のどこに対応し、実装でどう塞ぐかを見ていきます。
OWASP Top 10(2021)と実務の対応
| ID | 分類 | 実務での主な対策 |
|---|---|---|
| A01 | アクセス制御の不備 | サーバ側で毎回権限検証・既定は拒否(deny by default) |
| A02 | 暗号化の失敗 | 全面TLS化・パスワードは適切なハッシュ |
| A03 | インジェクション | プレースホルダ・出力エスケープ |
| A04 | 安全でない設計 | 設計段階の脅威分析 |
| A05 | セキュリティ設定ミス | 不要機能の無効化・公開範囲点検 |
| A06 | 脆弱で古いコンポーネント | 依存の棚卸し・継続更新 |
| A07 | 識別と認証の失敗 | MFA・試行回数制限・セッション管理 |
| A08 | 整合性の失敗 | 署名検証・配布元の確認 |
| A09 | ログと監視の不足 | 重要操作のログ・検知通知 |
| A10 | SSRF | アクセス先の限定・内部到達の遮断 |
以下、頻出どころを抜粋して掘り下げます。
A03 インジェクション:プレースホルダを徹底する
最も知られる SQLインジェクション。文字列連結でクエリを組むと、入力が「データ」ではなく「命令」として解釈されます。
# NG:文字列連結。' OR '1'='1 で条件が常に真になり全件返る
uid = request.args.get("id")
cur.execute("SELECT * FROM users WHERE id = '" + uid + "'")
# OK:プレースホルダ(プリペアドステートメント)で入力を「データ」として渡す
uid = request.args.get("id")
cur.execute("SELECT * FROM users WHERE id = %s", (uid,))
ポイントは「入力値とSQL文を分離する」こと。プレースホルダに渡すのは値であって、テーブル名やカラム名は渡せない点に注意します。動的にカラムを切り替えたい場合は、ユーザ入力を直接埋め込まず、許可リスト(allowlist)で対応します。
# 動的カラムは許可リストで検証してから組み立てる
ALLOWED_SORT = {"created_at", "name", "id"}
sort = request.args.get("sort", "id")
if sort not in ALLOWED_SORT:
sort = "id"
cur.execute(f"SELECT * FROM users ORDER BY {sort}")
XSS(クロスサイトスクリプティング)も発想は同じで、出力時に文脈に応じてエスケープします。テンプレートエンジンの自動エスケープを無効化(例:Jinja2 の | safe、React の dangerouslySetInnerHTML)する箇所が事故の起点になりがちなので、使う場合は理由をコメントで残す運用にすると見落としを減らせます。
A02 暗号化の失敗:TLSとパスワード保存
通信は全面TLS。HTTPは301でHTTPSへ寄せ、HSTS を付けます。
# HTTP は HTTPS へリダイレクト
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
# HSTS(事前に全サブドメインがHTTPS対応済みであることを確認してから付与)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
}
つまずき:
includeSubDomainsやpreloadは一度効くとブラウザにキャッシュされ、HTTP運用のサブドメインへ到達できなくなります。全サブドメインのHTTPS対応を確認してから、max-ageを小さい値で試すのが安全です。
パスポードは平文・可逆暗号・素のSHA-256で保存しないこと。ソルト付き・計算コストの高い専用アルゴリズムを使います。
# 例:パスワードハッシュ専用アルゴリズムを使う
from argon2 import PasswordHasher
ph = PasswordHasher()
stored = ph.hash("user-password") # 登録時
ph.verify(stored, "user-password") # 認証時(不一致は例外)
A05 セキュリティ設定ミス:既定値を点検する
最頻出の分類のひとつ。初期パスワード(admin/admin)放置、詳細エラーの本番露出、クラウドストレージの全体公開などが典型です。
- 本番ではスタックトレースを返さない(デバッグモードOFF)。
- 管理画面・不要なエンドポイントを公開しない。
- 最低限のセキュリティヘッダを付ける。
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "default-src 'self'" always;
つまずき:CSPは厳しく入れると既存のインラインスクリプトや外部CDNが軒並み止まります。まず
Content-Security-Policy-Report-Onlyで違反だけ収集し、影響を確認してから強制に切り替えるのが現実的です。
クラウド側も同様で、S3のようなオブジェクトストレージは「パブリックアクセスのブロック」を既定ONにし、必要なものだけ個別に開ける運用にします。
A06 古いコンポーネント:棚卸しと自動検知
依存ライブラリの既知脆弱性は、まず可視化から。CIに組み込むと放置を防げます。
# Node.js
npm audit
# Python(requirements の既知脆弱性をスキャン)
pip-audit
# OSSの依存・コンテナを横断スキャンしたい場合
trivy fs .
検出されてもすべてを即更新する必要はなく、実際に到達しうる経路か・深刻度はどうかで優先順位を付けます(後述のCVSS)。サポート切れ製品は更新自体が出ないため、置き換え計画を別立てにします。
A07 認証の失敗:MFAと試行回数制限
パスワード単体は、使い回し(パスワードリスト攻撃)や総当たりに弱い。MFAと試行回数制限を入れます。レート制限はnginx前段でも簡易に置けます。
# ログインエンドポイントを 1 IP あたり毎分10回に制限
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
location /login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://app;
}
セッション管理では、ログイン成功時にセッションIDを再発行(セッション固定対策)し、Cookieに HttpOnly Secure SameSite を付けます。
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
A10 SSRF:サーバからの外部アクセスを絞る
「画像URLを取得する」ような機能に内部アドレスを渡され、クラウドのメタデータ等を読み出されるのが典型です。ユーザ指定のURLでサーバが任意先へリクエストできる状態を作らないこと。
- スキームは
httpsのみ許可、リダイレクト追従を制限。 - 名前解決後のIPを検査し、プライベート/ループバック/メタデータ宛て(例:
169.254.169.254)を拒否。 - 可能なら到達先の許可リストで限定。
import ipaddress, socket
from urllib.parse import urlparse
def is_safe_url(url: str) -> bool:
p = urlparse(url)
if p.scheme != "https":
return False
ip = ipaddress.ip_address(socket.gethostbyname(p.hostname))
# プライベート・ループバック・リンクローカルは拒否
return not (ip.is_private or ip.is_loopback or ip.is_link_local)
つまずき:チェック時とリクエスト時で名前解決が変わる「DNSリバインディング」があるため、検証したIPに対して接続する/リダイレクトを追わない、といった実装上の配慮も必要です。
脆弱性はこう管理される(CVE・CVSS)
更新が大事な理由は、この流れを知ると腑に落ちます。
-
CVE:個々の脆弱性に付く世界共通の番号(例:
CVE-2024-12345)。各社・各ツールが同じ名前で参照できる。 - CVSS:深刻度を0〜10で数値化。対応の優先順位づけに使う。
- パッチ:公開直後は攻撃も増えるため、速やかな適用が重要。
国内では IPA・JPCERT/CC が届出受付や注意喚起を行っています。社内では「CVSSが高い × 公開資産で到達可能」を最優先にする、といった基準を決めておくと運用がぶれません。
多層防御(Defense in Depth)でまとめる
一点突破ではなく、各層で重ねて守ります。
- 設計・実装:入力検証/入力と命令の分離/出力の無害化(A03・A04)
- 更新:依存の棚卸しと継続更新(A06)
- 認証・権限:MFAと最小権限、既定値の見直し(A01・A05・A07)
- 入口:外部公開はWAFで遮断、脆弱性診断で点検
- 監視:重要操作のログと異常検知(A09)
弱点の把握には、広く網羅的にスキャンする脆弱性診断(健康診断)と、攻撃者視点で実際に侵入を試みるペネトレーションテスト(精密検査)があり、対象と目的で使い分け・併用します。
陥りやすい誤解
- 「正常に動いている=安全」ではない。動作確認は悪用への備えを保証しない。
- 「うちは小さいから狙われない」は危険。攻撃の多くは自動化され無差別。
- 「一度作れば終わり」ではない。新たな脆弱性は日々見つかるため運用が要。
- ツール導入だけで満足しない。設定・運用・更新が伴って初めて効く。
おわりに
OWASP Top 10は、Web開発・運用の事実上のチェック基準として広く使われています。本記事の対策は特別なものではなく、入力を疑う・更新を続ける・認証と権限を絞る・記録して監視するという基本の積み重ねです。まずは自社の公開資産で「到達可能な弱点」から優先的に潰していくのが現実的です。
分類・区分は OWASP Top 10(2021年版)に基づく一般的な説明です。最新の版や詳細は OWASP の公式情報をご確認ください。
株式会社ブレインディレクションは、ITソリューション/生成AI/クラウド/セキュリティの導入を支援しています(ISMS取得)。 https://www.brain-d.jp