はじめに
ReactやNext.jsなどのフレームワークが充実し、AIを使った開発も身近になりました。プログラミング経験が少なくても、ログインやデータ保存、ファイルアップロードなどを備えたWebアプリを作りやすくなっています。
しかし、「動くこと」と「安全に使えること」は別です。
画面上では問題なく動いていても、実装や設定の見落としによって、他人のデータを読み取られたり、アカウントを乗っ取られたりすることがあります。必ずしも高度な攻撃が必要なわけではなく、URLのIDを書き換えるだけで問題が表面化する場合もあります。
この記事では、Webアプリ開発で避けたい15の実装・設定と、その対策を紹介します。初めてWebアプリを公開する方や、既存の実装を見直したい方を想定しています。コード例はJavaScriptが中心ですが、基本的な考え方はPython、PHP、Javaなどでも共通です。
この記事は、セキュリティ対策のすべてを網羅したものではありません。各項目を公開前の確認の入り口として使い、扱うデータやサービスの用途に応じて対策を検討してください。主要なリスクの全体像はOWASP Top 10:2025(日本語)も参考になります。
1. ユーザー入力をそのままHTMLとして表示しない(XSS)
コメント欄やプロフィールなど、ユーザーの入力を画面に表示する場面はよくあります。次のコードでは、入力された文字列をinnerHTMLに渡しています。
// 危険な例:入力をHTMLとして解釈してしまう
const message = userInput;
document.getElementById("message").innerHTML = message;
innerHTMLは文字列をHTMLとして解釈します。悪意あるHTMLが含まれていると、イベントハンドラーなどを通じて、閲覧者のブラウザで不正なスクリプトが実行される可能性があります。これが**XSS(Cross-Site Scripting)**です。
対策
単なる文字列を表示したい場合は、textContentを使います。
// テキストとして表示する例
const message = userInput;
document.getElementById("message").textContent = message;
Reactの通常のJSX式でも文字列はエスケープされます。ただし、dangerouslySetInnerHTMLなどでHTMLを直接挿入する場合は、別途対策が必要です。
HTMLの表示が必要なら、DOMPurifyなどのサニタイズライブラリで許可する要素・属性を制限します。URL、CSS、JavaScriptなどに値を埋め込む場合は、それぞれの文脈に合った検証やエンコードが必要です。
まずは「テキスト表示のためにHTMLを直接挿入していないか」を確認しましょう。
参考:OWASP XSS Prevention Cheat Sheet
2. SQLを文字列連結で組み立てない(SQLインジェクション)
次のコードは、ユーザーが入力したemailをそのままSQLに埋め込んでいます。
// 危険な例:入力によってSQLの意味が変わる可能性がある
const sql = `SELECT * FROM users WHERE email = '${email}'`;
const result = await db.query(sql);
入力によってSQLの構造が変わると、意図しないデータの取得や更新につながります。この問題をSQLインジェクションと呼びます。
対策
プレースホルダーを使い、SQL文と入力データを分離します。PostgreSQLのpgライブラリでは、次のように書けます。
// pgのパラメータ化クエリの例
const result = await db.query("SELECT * FROM users WHERE email = $1", [email]);
この書き方なら、入力された文字列がSQLの命令として解釈されるのを防げます。ORMを使っていても、文字列連結で生SQLを組み立てる箇所があれば注意が必要です。
テーブル名やカラム名は、通常の値と同じ方法ではパラメータ化できません。動的に切り替える場合は、あらかじめ許可した識別子だけを選べるようにします。データベース接続ユーザーの権限も、必要最小限に絞りましょう。
参考:node-postgres:Queries、OWASP SQL Injection Prevention Cheat Sheet
3. Cookie認証を使うならCSRF対策を省かない
ログイン中のユーザーが、別のWebサイトを開いたとします。そのサイトからプロフィール変更などのリクエストを送られたとき、ブラウザは条件によって、リクエスト先のCookieを自動的に送信します。
サーバーがCookieだけで操作を受け付けると、ユーザーが意図していない変更まで実行される可能性があります。これが**CSRF(Cross-Site Request Forgery)**です。
対策
状態を変更するリクエストでは、フレームワークのCSRF対策機能などを使い、サーバー側で操作の正当性を検証します。
- CSRFトークンを検証する
-
Originなどのヘッダーから、許可した送信元かを検証する - Cookieの
SameSite属性を用途に合わせて設定する - GETリクエストではデータを変更しない
SameSite=LaxやSameSite=Strictは有効な防御策ですが、適用条件を確認せずに、これだけで十分と判断しないようにしましょう。
CSRFのリスクは、認証情報の送信方法にも依存します。Cookieを自動送信する方式と、JavaScriptが明示的に認証ヘッダーを付ける方式では、攻撃が成立する条件が異なります。
参考:OWASP CSRF Prevention Cheat Sheet
4. パスワードを平文やSHA-256だけで保存しない
パスワードをそのままデータベースに保存すると、データベースが流出した際に、パスワードも知られてしまいます。
# 避けたい保存例
email: user@example.com
password: mypassword123
SHA-256でハッシュ化するだけでも不十分です。SHA-256は高速に計算できるため、流出したハッシュに対して大量のパスワード候補を試す攻撃のコストを、十分に高められません。
対策
パスワード保存用のアルゴリズムを、適切なコスト設定で使います。新規開発ではArgon2idを検討し、利用できない場合はscryptなどを選びます。bcryptは主に既存システムで使われ、PBKDF2はFIPS対応などの要件がある場合の選択肢になります。
パスワードごとに異なるランダムなソルトも必要です。一般的なパスワードハッシュライブラリはソルトの生成・保存を扱えるため、その機能を利用し、独自の保存方式は避けましょう。
保存時の保護に加えて、ログイン試行の制限や多要素認証も検討してください。適切にハッシュ化しても、推測しやすいパスワードでのログインを防げるわけではありません。
参考:OWASP Password Storage Cheat Sheet
5. ログイン済みでも他人のデータにアクセスさせない(認可)
認証と認可は、役割が異なります。
| 用語 | 確認すること |
|---|---|
| 認証(Authentication) | 誰なのか |
| 認可(Authorization) | その人が、対象のデータや機能を使ってよいか |
例えば、次のAPIで自分の文書を取得できるとします。
GET /api/documents/100
IDを101に書き換えると、別のユーザーの文書まで取得できてしまうなら、認可に問題があります。これは**IDOR(Insecure Direct Object Reference)の典型例で、APIではBOLA(Broken Object Level Authorization)**という用語も使われます。
対策
要求されたデータへのアクセス権限を、サーバー側で確認します。自分が所有する文書だけを取得できるサービスなら、取得条件に所有者の確認を含めます。
SELECT *
FROM documents
WHERE id = $1
AND owner_id = $2;
$1には文書ID、$2には認証済みセッションから特定したユーザーIDを渡します。ブラウザから送られてきたユーザーIDを、そのまま所有者の確認に使ってはいけません。共同編集がある場合は、共有権限も確認します。
画面上でボタンを隠したり、IDを推測しにくくしたりするだけでは、アクセス制御になりません。取得・更新・削除を含む、すべての保護対象操作で認可を強制しましょう。
6. セッションIDやトークンの保存・失効を軽く扱わない
セッションIDやアクセストークンが盗まれると、攻撃者がユーザーになりすましてアクセスできる可能性があります。例えばlocalStorageに保存したトークンは、同じオリジンでXSSが成立すると読み取られる危険があります。
対策
一般的なWebアプリでは、サーバー側でセッションを管理し、ブラウザには適切な属性を設定したCookieを渡す構成が選択肢になります。
Set-Cookie: __Host-session=RANDOM_SESSION_ID; Path=/; Secure; HttpOnly; SameSite=Lax
これは属性の例です。実際には、セッションIDを十分に予測困難な乱数で生成し、Cookieの設定をアプリの構成に合わせます。
| 属性 | 役割 |
|---|---|
Secure |
CookieをHTTPS通信で送信する |
HttpOnly |
JavaScriptからCookieを読み取れなくする |
SameSite |
クロスサイトリクエスト時のCookie送信を制御する |
Path |
Cookieを送信するパスの範囲を指定する |
__Host-という接頭辞を使うには、Secure、Path=/を設定し、Domain属性を指定しないなどの条件を満たす必要があります。
HttpOnlyでもXSSそのものは防げません。攻撃スクリプトがログイン中のブラウザから操作を実行する可能性は残るため、XSS対策とCSRF対策も必要です。
保存先だけでなく、次の点も確認しましょう。
- ログイン時などにセッションIDを更新する
- セッションの有効期限を管理する
- ログアウト時にサーバー側でセッションを無効化する
- JWTを使う場合は、署名・有効期限・発行者・利用対象を検証し、必要な失効方法を設計する
保存方法はアプリの構成や想定する攻撃によって選びます。「Cookieなら安全」「localStorageなら必ず脆弱」と一律に判断しないことが大切です。
参考:OWASP Session Management Cheat Sheet、MDN:Set-Cookie
7. CORSエラーを消すためだけに許可範囲を広げない
フロントエンドからAPIを呼び出すと、次のようなエラーに遭遇することがあります。
Access to fetch has been blocked by CORS policy
**CORS(Cross-Origin Resource Sharing)**は、異なるオリジンからのリクエストに対し、ブラウザがレスポンスをJavaScriptに公開してよいかを判断する仕組みです。オリジンは、スキーム・ホスト名・ポートの組み合わせで決まります。
対策
レスポンスの共有が必要なオリジンだけを許可します。
Access-Control-Allow-Origin: https://app.example.com
Cookieを含むクロスオリジン通信では、クライアント側の認証情報の送信設定に加え、サーバー側で明示的なオリジンとAccess-Control-Allow-Credentials: trueの設定が必要です。CookieのSameSite属性やブラウザのCookie制限にも影響されます。
リクエストのOriginを無条件で許可する実装は避け、許可リストと照合します。なお、Access-Control-Allow-Credentials: trueとAccess-Control-Allow-Origin: *を組み合わせても、ブラウザはそのレスポンスをJavaScriptに公開しません。
CORSはAPIの認証・認可の代わりにはなりません。
curlなどからの直接アクセスは制限できず、CORSでレスポンスを読めなくても、リクエスト自体がサーバーに届く場合があります。CSRF対策とも分けて考えましょう。
認証不要の公開データを返すAPIでは、Access-Control-Allow-Origin: *が適切な場合もあります。用途に合った設定かを確認してください。
参考:MDN:Cross-Origin Resource Sharing(CORS)
8. 秘密のAPIキーをフロントエンドに埋め込まない
ブラウザに配信するJavaScriptに秘密のAPIキーを書くと、利用者から参照できます。
// ブラウザに配信するコードには書かない
const API_KEY = "YOUR_SECRET_API_KEY";
難読化しても秘密にはできません。また、環境変数に入れていても、フロントエンドのビルド時にJavaScriptへ埋め込まれれば公開されます。
対策
秘密情報が必要な処理は、原則としてサーバー側で実行します。
ブラウザ
↓
自分のWebアプリのAPI
↓ 秘密のAPIキーを使って呼び出す
外部サービス
サーバーでは環境変数やシークレット管理サービスで秘密情報を扱い、ソースコードやGitリポジトリへ含めないようにします。
中継するAPIにも認証・認可・利用量制限が必要です。誰でも無制限に呼び出せると、キー自体を隠していても、外部サービスを不正利用される可能性があります。
ブラウザで使うことを前提にした公開識別子や公開APIキーは、この限りではありません。その場合も、提供元が指定する利用制限を設定しましょう。
9. アップロードされたファイルを信用しない
image.pngという名前でも、安全なPNG画像とは限りません。拡張子や、アップロード時に送られるContent-Typeは偽装できます。
受け取ったファイルをそのまま公開ディレクトリへ保存すると、配信方法やサーバー設定によっては、スクリプト実行、XSS、他のファイルの上書きにつながります。巨大なファイルや圧縮ファイルによるリソース枯渇も考慮が必要です。
対策
- 許可する形式を限定し、拡張子だけでなく実際の内容も検証する
- 受信サイズに加え、展開後のサイズや画像の画素数、処理時間なども制限する
- 保存ファイル名をサーバー側で生成する
- 可能ならWeb公開ディレクトリの外に保存し、配信処理を通して公開する
- 必要に応じてマルウェア検査や画像の再エンコードを行う
- 配信時の
Content-Typeなどを適切に設定する
ファイルを保存・取得する際の認可も必要です。非公開ファイルなら、保存先が推測しにくいだけで安心せず、閲覧権限を確認してください。
検証用の画像処理ライブラリにも脆弱性はあり得ます。更新を継続し、検証処理そのものにもリソース制限を設けましょう。
参考:OWASP File Upload Cheat Sheet
10. ユーザー指定のURLにサーバーから無条件でアクセスしない(SSRF)
URLを入力するとページのタイトルを取得してくれる機能を考えます。
POST /api/preview
{
"url": "https://example.com"
}
サーバーがアクセス先を制限していないと、攻撃者が内部ネットワークのサービスや、クラウド環境のメタデータサービスへリクエストを送らせる可能性があります。これが**SSRF(Server-Side Request Forgery)**です。
対策
- 可能ならアクセス先を許可リストで限定する
- 必要なURLスキームだけを許可する
- ループバック、プライベートIP、リンクローカルなどへのアクセスを制限する
- DNSの名前解決結果と、実際の接続先を検証する
- リダイレクトを無効にするか、遷移先にも同じ制限を適用する
- タイムアウトやレスポンスサイズの上限を設ける
- ネットワーク側でも不要な宛先への通信を制限する
URL文字列だけの検証では不十分です。IPv4とIPv6の両方を扱い、DNSの結果が検証時と接続時で変わるケースも考慮する必要があります。
任意の外部URLを取得する機能では、単純な文字列チェックで済ませず、接続先の制御を含めて設計しましょう。
参考:OWASP SSRF Prevention Cheat Sheet
11. HTTPSを使っているだけで安全だと思わない
HTTPSは、TLSによって通信中の盗聴や改ざんを防ぐための重要な仕組みです。ただし、XSS、SQLインジェクション、認可の不備など、アプリケーション内部の問題は防げません。
対策
HTTPSを適切に設定したうえで、用途に応じたHTTPセキュリティヘッダーも検討します。
| ヘッダー | 主な役割 |
|---|---|
Strict-Transport-Security(HSTS) |
ブラウザに、対象ホストへのHTTPS接続を要求する |
Content-Security-Policy(CSP) |
スクリプトなどの読み込み・実行や埋め込みを制限する |
X-Content-Type-Options |
MIMEタイプの推測を抑制する |
Referrer-Policy |
参照元情報の送信範囲を制御する |
CSPはXSSの影響を軽減する追加防御になりますが、出力時のエスケープやサニタイズの代わりにはなりません。
HSTSは、ブラウザがポリシーを受け取った後の接続に適用されます。長い有効期間やincludeSubDomainsの指定は、すべての対象ホストでHTTPSを維持できるか確認してから導入してください。
設定例をそのまま貼るのではなく、アプリの動作と運用への影響を確かめて適用しましょう。
参考:MDN:Strict-Transport-Security
12. 依存パッケージを無条件に信用しない
ライブラリは開発を助けてくれますが、それ自体に脆弱性が見つかったり、配布経路が侵害されたりする可能性があります。直接使っているパッケージだけでなく、その依存先にも注意が必要です。
対策
Node.jsのプロジェクトでは、例えば次のコマンドで既知の脆弱性を確認できます。
npm audit
ただし、報告がなければ安全というわけではありません。未発見の脆弱性や、改ざんされたパッケージをすべて検出できるものではないためです。
- 不要な依存パッケージを増やさない
- パッケージの提供元や保守状況を確認する
- ロックファイルをバージョン管理する
- CIなどで依存関係を継続的に検査する
- 更新内容を確認し、テストしてから適用する
- CIやビルド環境に渡す権限・秘密情報を必要最小限にする
npmでは、ロックファイルに基づくインストールにnpm ciを使えます。ただし、ロックファイルは再現性を高めるものであり、パッケージの安全性を保証するものではありません。
依存パッケージのコードは、実行時だけでなく、インストール時やビルド時にも動くことがあります。開発・ビルド環境も保護の対象です。
13. エラーの詳細をそのままユーザーに返さない
開発時に役立つ詳しいエラーメッセージも、本番で公開すると、内部のファイルパス、SQL文、テーブル名、設定情報などを漏らす原因になります。
対策
利用者には必要最小限の情報を返し、詳細はアクセスを制限した内部ログで確認します。
{
"error": "internal_server_error",
"message": "処理に失敗しました",
"requestId": "req-12345"
}
requestIdのような識別子があれば、問い合わせと内部ログを関連付けられます。ただし、内部ログにも秘密情報をそのまま残さないようにします。
エラーが発生した後の動作も重要です。例えば、権限の確認に失敗したときに「確認できなかったので許可する」としてはいけません。
セキュリティ上の判断に失敗した場合は、原則としてアクセスを許可しない。 この考え方を、フェイルクローズと呼びます。
14. ログにパスワードやトークンを記録しない
リクエスト全体をログ出力すると、パスワードや個人情報まで残ることがあります。
// 内容を確認せずに丸ごと出力しない
console.log(request.body);
HTTPヘッダーの一括出力でも、セッションCookieやアクセストークンが記録される可能性があります。ログは外部の収集サービスに転送されることもあり、閲覧できる人やシステムが増える点にも注意が必要です。
対策
パスワード、アクセストークン、セッションID、秘密のAPIキー、秘密鍵、不必要な個人情報は、原則としてそのまま記録しません。調査に必要な項目を選び、構造化して記録します。
{
"event": "login_failed",
"requestId": "req-12345",
"reason": "invalid_credentials",
"timestamp": "2026-10-13T10:00:00Z"
}
一方で、ログを残さないと不正操作の発見や調査が難しくなります。ログイン失敗や管理者操作などの重要なイベントを記録し、保存期間、閲覧権限、異常時のアラートまで設計しましょう。
15. APIを無制限に呼び出せる状態で公開しない
正しく動くAPIでも、大量に呼び出されると、サーバー負荷や費用が増大します。ログイン試行、パスワードリセットメールの送信、高額な外部APIの利用、ファイルアップロードなどは、特に制限が必要です。
対策
APIの用途に応じて、レート制限とリソース制限を組み合わせます。
| 制限の対象 | 対策の例 |
|---|---|
| リクエスト回数 | IPアドレスやアカウント単位の制限 |
| ログイン試行 | 失敗時の段階的な待機時間 |
| 入力・出力の大きさ | 本文、ファイル、検索件数の上限 |
| 外部サービスの利用 | 利用回数・費用の上限 |
| 処理の負荷 | タイムアウト、同時実行数、キュー長の上限 |
呼び出し頻度の制限にはHTTP 429(Too Many Requests)、本文サイズの制限にはHTTP 413(Content Too Large)など、状況に合ったステータスを返します。
入力値の検証や制限は、必ずサーバー側でも行ってください。ブラウザのJavaScriptは変更でき、APIはcurlなどから直接呼び出せます。
IPアドレス単位の制限だけでは、複数のIPを使う攻撃に対応しきれず、同じネットワークの正規ユーザーにも影響する場合があります。アカウント単位の制限なども組み合わせ、利用実態に合わせて調整しましょう。
よくあるセキュリティの誤解
ここまでの内容を、開発中に起こりやすい思い込みと対応させると、次のようになります。
| 誤解 | 確認したいこと |
|---|---|
| HTTPSなら安全 | 通信の保護に加えて、アプリ内部の対策も必要 |
| JWTなら安全 | 署名・有効期限などの検証と、失効方法の設計が必要 |
| ReactならXSSは起きない | HTMLの直接挿入など、標準の保護を外す箇所に注意 |
| CORSでAPIを保護できる | サーバー側の認証・認可が必要 |
| ログイン済みならアクセスさせてよい | 対象データ・操作ごとの認可が必要 |
| フォームで検証しているから安全 | APIへの直接リクエストもサーバー側で検証する |
| 環境変数なら秘密にできる | ブラウザに配信されるコードへ埋め込まれていないか確認する |
npm auditで問題がなければ安全 |
検出範囲を理解し、依存関係の管理を続ける |
| AIが書いたコードだから正しい | 生成されたコードにもレビューとテストが必要 |
対策を一つ入れただけで、すべての問題を防げるわけではありません。複数の対策を組み合わせ、一つが突破された場合にも被害を抑える考え方を**多層防御(Defense in Depth)**と呼びます。
公開前に確認したいチェックリスト
自分のアプリに該当する項目について、設定の有無だけでなく、実際の動作まで確認してみてください。
入力と出力
- ユーザー入力をHTMLとして不用意に表示していない
- SQLの値をパラメータ化し、動的な識別子を制限している
- サーバー側で入力値を検証している
- アップロードの形式・サイズ・保存先・閲覧権限を制限している
- 外部URLの取得で、実際の接続先やリダイレクト先を制限している
認証と認可
- パスワードを適切なアルゴリズムとコスト設定でハッシュ化している
- ログイン試行に制限がある
- 保護対象の各APIで認証と認可を検証している
- 権限のない他人のデータを取得・更新・削除できない
- セッションやトークンの保護・有効期限・失効方法を確認している
- Cookie認証を使う場合、適切なCSRF対策がある
設定と運用
- HTTPSを適切に設定している
- CORSの許可範囲が用途に合っている
- 秘密情報をブラウザやGitリポジトリへ含めていない
- HTTPセキュリティヘッダーの設定と動作を確認している
- 依存パッケージを継続的に管理・検査している
- 本番環境で内部エラーを公開せず、権限確認の失敗時には拒否している
- ログにパスワードやトークンを記録していない
- APIにレート制限やリソース制限がある
- 不正操作を検知するログやアラートがある
チェックが付くこと自体が、安全性の保証になるわけではありません。特に認可は、別アカウントでIDを書き換えてアクセスするなど、拒否されるべき操作が実際に拒否されるかを確かめましょう。
AIにコードを書いてもらう場合も、確認は必要
AIに実装を依頼する際も、機能だけでなくセキュリティ要件を伝えることが大切です。例えば、次のように依頼できます。
Webアプリを実装してください。
・入力値はサーバー側でも検証する
・ユーザー入力を表示する箇所でXSS対策を行う
・保護対象のAPIで認証と認可を確認する
・ユーザーは、自分に許可されたデータだけを取得・更新・削除できるようにする
・Cookie認証を使う場合はCSRF対策を行う
・セッションの有効期限とログアウト時の無効化を実装する
・エラー応答やログに秘密情報を含めない
・外部APIの利用やアップロードには制限を設ける
残るセキュリティ上の懸念と、確認が必要な点も説明してください。
ただし、指示したことや、AIが「対策済み」と説明したことだけで確認を終えないようにしましょう。実際のコードをレビューし、必要なテストを行います。
正常系に加えて、未ログイン、別ユーザー、期限切れのセッション、サイズ超過など、受け付けてはいけない操作を正しく拒否できるかを確認してください。
おわりに
今回紹介した問題の多くは、日常的な実装や設定に関わるものです。入力をどう扱うか、誰にどのデータを許可するか、秘密情報をどこへ保存するか。こうした判断が、アプリの安全性につながります。
まずは公開前チェックリストを使い、自分のアプリで説明できない項目を見つけるところから始めてみてください。機能追加、依存パッケージの更新、運用環境の変更に合わせて見直しを続けることも大切です。
「動くものを作る」に加えて、「安心して使えるものを作る」。 公開前には、利用者が意図しない操作や、権限のないアクセスも想定して確認しましょう。
参考資料
実装や設定の詳細を確認する際は、利用するフレームワークの公式ドキュメントと併せて、次の資料を参照してください。
- OWASP Top 10:2025(日本語)
- OWASP Cheat Sheet Series
- XSS Prevention Cheat Sheet
- SQL Injection Prevention Cheat Sheet
- CSRF Prevention Cheat Sheet
- Password Storage Cheat Sheet
- Session Management Cheat Sheet
- File Upload Cheat Sheet
- SSRF Prevention Cheat Sheet
- MDN:Cross-Origin Resource Sharing(CORS)
- node-postgres:Queries