Web開発の現場では、時々「あれ、これって普通にまずいのでは...?」と思う実装に出くわすことがあります。技術的に高度な話ではなく、むしろ「言われてみれば確かに」というレベルの話なのだけど、意外と見落とされがちなポイントをいくつか挙げてみます。
1.パスワードをそのまま保存してしまう
データベースに password というカラムがあって、そこに「password123」みたいな文字列がそのまま入っている状態。
何が問題なのか:
データベースを見られる人(開発者、インフラ担当者、または侵入者)が、ユーザーのパスワードをそのまま知ることができてしまう。多くの人は複数のサービスで同じパスワードを使い回しているため、一箇所から漏れると他のサービスでも被害が広がる。
どうすればいいのか:
パスワードは「ハッシュ化」という一方通行の変換をかけて保存する。bcryptやArgon2といったアルゴリズムを使えば、元のパスワードに戻すことができない形で保存できる。ログイン時は、入力されたパスワードを同じ方法でハッシュ化して、保存されているハッシュと比較する。
2.個人情報をURLに含めてしまう(GETメソッド)
https://example.com/user?name=山田太郎&email=taro@example.com&phone=090-1234-5678 みたいなURLでページを表示している状態。
何が問題なのか:
URLはブラウザの履歴、サーバーのアクセスログ、プロキシサーバー、リファラー(どのページから来たかの情報)など、様々な場所に記録される。つまり個人情報が色々なところにバラまかれてしまう。URLを誰かに見られただけでも情報が漏れる。
どうすればいいのか:
個人情報を送る時はPOSTメソッドを使う。POSTならデータはリクエストボディに入るので、少なくともURLには表示されない。それでも通信はHTTPSで暗号化することが前提。
3.他のユーザーの情報が見えてしまう(認可の欠如)
/api/users/123/profile にアクセスすると、ユーザーID 123番の人のプロフィールが表示される。ここで「じゃあ124番にしたらどうなる?」とURLを書き換えると、他人の情報が見えてしまう。
何が問題なのか:
これは「認可(Authorization)」が抜けている状態。「ログインしているか」のチェック(認証)だけでは不十分で、「その情報にアクセスする権限があるか」のチェックが必要。URLのパラメータを変えるだけで他人のデータにアクセスできるのは、玄関の鍵は一応かかっているけど、部屋の鍵は全部開きっぱなしみたいなもの。
どうすればいいのか:
サーバー側で「このリクエストをしているユーザーが、このデータにアクセスする権限を持っているか」を必ずチェックする。例えば、ログイン中のユーザーIDとリクエストされたユーザーIDが一致するか、管理者権限を持っているか、などを確認する。
4.他のユーザーの情報を編集できてしまう
上記の「見えてしまう」問題の更新版。/api/users/123 にPUTやPATCHリクエストを送ると、他人のプロフィールや設定を書き換えられてしまう。
何が問題なのか:
情報を見られるだけでも問題だが、書き換えられるのはもっと深刻。メールアドレスを変更されてアカウントを乗っ取られたり、住所を書き換えられて商品が別の場所に送られたり、実害が発生する。
どうすればいいのか:
読み取りと同様に、書き込み操作でも必ず権限チェックを行う。「このユーザーはこのデータを変更できるのか?」という確認を、API側で毎回実装する。フロントエンドで制御するだけでは不十分(ブラウザの開発者ツールで簡単に回避できる)。
5.クレジットカード情報をログに出力してしまう
デバッグのために console.log(requestBody) や logger.info(request) みたいなコードを書いて、リクエスト内容をログに残している。ここにクレジットカード番号やセキュリティコードが含まれている。
何が問題なのか:
ログファイルは開発者、運用担当者、場合によっては監視ツールなど、多くの人や システムがアクセスできる。クレジットカード情報は「決済時に一瞬だけメモリを通過して、すぐに忘れる」べきもの。ログに残すと、その情報が長期間、広範囲に晒されることになる。
どうすればいいのか:
機密情報(カード番号、パスワード、個人番号など)は、そもそもログに出力しない設計にする。どうしてもリクエスト全体をログに残したい場合は、機密情報をマスキング(一部を***に置き換える)するか、該当フィールドを除外する処理を入れる。
6.CSRFトークンをコメントアウトしてしまう
フレームワークがデフォルトで提供しているCSRF(クロスサイトリクエストフォージェリ)対策機能を、「動かないから」「面倒だから」という理由で無効化している。
何が問題なのか:
CSRF攻撃は、ユーザーが知らないうちに、悪意あるサイトから正規サイトへリクエストを送らせる攻撃。例えば、罠サイトを開いただけで「退会処理」や「パスワード変更」が実行されてしまう。フレームワークのCSRF対策は、まさにこれを防ぐためにある。
どうすればいいのか:
フレームワークのCSRF対策機能は基本的に有効にしておく。動かない場合は、設定を見直すか、正しい実装方法を調べる。「とりあえずコメントアウト」は、セキュリティホールを自分で開けているようなもの。
7.変数名を連番にする(user1, user2, user3...)
const user1 = getUserById(1);
const user2 = getUserById(2);
const user3 = getUserById(3);
何が問題なのか:
これ自体がセキュリティ問題というわけではないが、コードの可読性が著しく低い。user1が何を表しているのか、コードを読んでも分からない。「現在のユーザー」なのか「対象のユーザー」なのか「比較元のユーザー」なのか、文脈を追わないと理解できない。結果として、バグや脆弱性を生みやすくなる。
どうすればいいのか:
変数名は「何を表しているか」が分かる名前にする。currentUser、targetUser、loggedInUserなど、役割が明確な名前をつける。配列ならusersのように複数形にして、users[0]でアクセスする。
8.公開ディレクトリに機密ファイルを置いてしまう
アップロードされた本人確認書類(免許証、マイナンバーカードなど)の画像を、/public/uploads/license_001.jpg みたいに、誰でもアクセスできる場所に保存している。
何が問題なのか:
/public配下のファイルは、URL を知っていれば誰でもアクセスできる。ファイル名が連番なら、license_002.jpg、license_003.jpg と試すだけで、他人の本人確認書類を見ることができてしまう。個人情報保護法違反レベルの情報漏洩につながる。
どうすればいいのか:
機密ファイルは公開ディレクトリの外に保存し、アクセス時は必ず認証・認可を通す。例えば、/api/documents/123 のようなエンドポイントを作り、サーバー側で「このユーザーはこのファイルを見る権限があるか」を確認してから、ファイルを返す。ファイル名も推測されにくいランダムな文字列(UUID等)にする。
9.WebhookやAPIの署名検証をしない
決済サービスからの通知(Webhook)を受け取るエンドポイントで、「本当にその決済サービスからのリクエストか」を確認せずに、送られてきたデータをそのまま信用して処理している。
何が問題なのか:
悪意ある第三者が「入金完了」という偽の通知を送ることで、実際には払っていないのに商品やサービスを受け取れてしまう。決済サービスは通常、「このリクエストが本物だと証明する署名」を付けてくるので、それを検証しないと偽装リクエストを防げない。
どうすればいいのか:
Webhookを受け取るエンドポイントでは、必ず署名検証を実装する。各決済サービスのドキュメントに検証方法が書かれているので、それに従って実装する。検証に失敗したリクエストは無視するか、エラーを返す。
10.テスト用のAPIを本番環境に残してしまう
開発中に作った「テスト用にポイントを追加する」「デバッグ用にユーザーステータスを変更する」みたいなAPIが、本番環境でもアクセス可能になっている。
何が問題なのか:
これらのテスト用APIは、通常の業務フローを無視して直接データを操作できるため、悪用されると致命的。「簡単にコールできる設計」になっているため、認証や認可も緩いことが多い。
どうすればいいのか:
テスト用のコードは、環境変数やビルド設定で、本番環境では完全に無効化する。または、本番環境にデプロイする前にコードごと削除する。「念のため残しておく」は危険。
おわりに
ここに挙げた例は、どれも「高度な技術」ではなく、「基本的な考え方」の問題です。でも、実際の現場では意外とこういう実装に出会うことがあります。
大事なのは、「ユーザーは信頼できる入力をしてくれる」「URLのパラメータは誰も書き換えない」「フロントエンドで制御すれば十分」といった性善説や楽観的な前提を持たないことです。
「もし悪意ある人がこのシステムを使ったら?」「もしパラメータを書き換えられたら?」という視点を常に持つことが、セキュアなシステムを作る第一歩になります。