ここのところ、静的診断や動的診断などを経て、対策はばっちりしているつもりでした。ただ、Claude Fable 5.2 はセキュリティ診断に強いと言われていたので、試してみるしかないと思いました。出たら嫌だなと思っていたら、本当に問題が見つかり、慌てて直しました。

CSRF 対策、CSP、外部スクリプト、Docker を中心に、やったことと確かめ方をまとめます。

これは私の実体験に基づく話です。その点にご留意ください。

注意: この記事で触れる診断ツールは、自分で管理している環境にだけ使ってください。許可のない相手には使わないでください。本番環境や、ほかの人と共有している環境でも使わないでください。

この記事で分かること

  • フレームワークに頼らない自作ブログで、守りを固めるときの考え方
  • CSRF 対策を「2枚」にする方法
  • CSP を「報告だけ」から「実際に止める」に切り替える手順
  • Docker のイメージを小さく・固くする方法(1150MB → 483MB)
  • 直したあとに、毎回同じ手順で確かめるやり方
  • AI(Claude Code)と進めたときのトークン数

はじめに

このブログは、大きなフレームワークを使わずに、ログイン・記事の編集・お問い合わせなどをほとんど私自身が実装しています。 といってもfacebook社製のossを利用しており、全てではありません。

自前ということは、有名なフレームワークが最初から用意してくれる「安全のための仕組み」を、全部自分で用意しないといけないということです。

今回は Claude Fable 5.2 の性能を確認するため、ソースコードをまるごと見直して、見つかったものをその日のうちに直しました。本当に出るとは思わなかったので慌てました(;'∀')

やったことを残しておきます。

何を調べたか

Fable 5.2 に調査依頼を出してソースコード全体を、ブラウザから入力されるところ、保存されるところ、画面に出るところまで順に隅々まで追いかけました。

flowchart LR A["入力が入ってくる"] --> B["保存される"] B --> C["画面に出る"]

見たのは、よくある5つの分類です。

  1. インジェクション(SQL インジェクション・XSS・コマンドの実行・パストラバーサル)
  2. 認証とセッション(パスワードの保存方法・セッション・CSRF)
  3. ファイルのアップロード
  4. エラーとログ(余計な情報を出していないか)
  5. 設定(セキュリティヘッダ・秘密情報の扱い)

結果

幸いなことに、致命的なもの(Critical)はありませんでした。

ただ、「今すぐ破られるわけではないけれど、守りが1枚しかない」ところがいくつか見つかりました。放置するのも気持ちが悪いので、今回、すべて対応しています。

直したこと

1. CSRF 対策を2枚にした

用語メモ: CSRF 別のサイトに仕込んだわなから、ログイン中の人のブラウザを使って、勝手に操作させる攻撃です。

今までは、Cookie の SameSite 属性の1枚だけで守っていました。これに加えて、データを変えるリクエストを受けたときに、ブラウザが付けてくる送信元を示すヘッダを見て、このサイトの画面から送られたものかを確かめるようにしました。別のサイトからの送信は断ります。

Cookie の設定と、送信元の確認の2枚になったので、どちらかが効かない状況でも守れます。

flowchart TB A["別のサイト<br/>からの送信"] --> B["1枚目: Cookie の<br/>属性で止める"] B -->|"すり抜けた場合"| C["2枚目: 送信元を<br/>確かめて断る"] C --> D["このサイトの画面<br/>からの送信だけ<br/>受け付ける"]

2. CSP を「報告だけ」から「実際に止める」にした

用語メモ: CSP と nonce CSP(Content Security Policy)は、ページで動かしてよいスクリプトをブラウザに伝える仕組みです。万一、ページにスクリプトが紛れ込んでも、動かさないようにできます。XSS に対する最後の防壁です。 nonce は、アクセスのたびに作るランダムな値のことです。この値を付けたスクリプトだけを動かします。

このブログでは、アクセスのたびに nonce を作り、その値を付けたスクリプトだけを動かす形にしています。広告が動くように、Google が AdSense 向けに案内している形に合わせました。

今までは、広告が止まるのが怖くて「違反を報告するだけ」の設定でした。実際に止める前に、自分のコードで止まってしまうところを探して直しました。

  • href="javascript:..." のリンク
  • onclick="..." のように、HTML に直接書いたイベント

どちらも nonce の方式では動かなくなるので、addEventListener でイベントを登録する形に置き換えました。

確かめるために、ヘッドレスの Chrome で CSP を実際に効かせて、全部の画面を開いて操作しました。わざと紛れ込ませたスクリプトが、ちゃんと止まることも確かめています。

flowchart LR A["報告だけで入れる"] --> B["報告を見る"] B --> C["自分のコード<br>で引っかかる所を直す"] C --> D["実際に止める"]

CSP を入れるときのコツ: いきなり止めずに、まず「報告だけ」で入れて、報告を見ながら自分のコードで引っかかるところをつぶす。それから止める。こうすると、サイトを壊さずに入れられます。

3. 外部の CDN からのスクリプトの読み込みをやめた

用語メモ: CDN と SRI CDN は、ファイルを世界中のサーバーから配信する仕組みです。SRI(Subresource Integrity)は、読み込むファイルのハッシュを integrity 属性に書いておき、書き換えられていたら実行しないようにする仕組みです。

一部のスクリプトを、外部の CDN からそのまま読み込んでいました。CDN が乗っ取られると、知らないスクリプトがこのサイトで動いてしまいます。

公式の配布物を、公式に公開されているハッシュと照合してから取り出し、自分のサーバーから配信するようにしました。さらに SRI を付けて、ファイルが書き換えられていたら実行しないようにしています。

つまずいたのは、Windows の git の改行の自動変換です。改行が変わるとハッシュが合わなくなるので、外部のライブラリのファイルだけ変換しないように、.gitattributes で指定しました(-text)。

4. アプリが自分のプログラムを書き換えられないようにした

Docker のコンテナで、プログラムのフォルダを書き込みできる状態で使っていました。アプリに穴が1つあると、プログラムを書き換えられて、再起動しても残ってしまいます。

アプリが本当に書き込む必要のある場所を調べて、そこだけ書き込めるようにし、残りは読み取り専用でマウントしました(Docker の :ro)。

5. Docker のイメージを小さく・固くした

用語メモ: マルチステージビルド Docker のイメージを作る手順を、複数の段階に分ける方法です。ビルドする段階と動かす段階を別にして、動かすほうには必要なものだけを入れます。

動かすためのイメージに、コンパイラやダウンロード用のツールが残っていました。乗っ取られたときに、攻撃者の道具になります。

ビルドを2段階に分けました(マルチステージビルド)。動かすほうには、ビルドの結果と、実行に必要なものだけを入れています。必要な共有ライブラリは ldd で調べました。ベースのイメージも latest をやめて、版を固定しました。

flowchart LR A["ビルドの段階<br/>コンパイラなどを使う"] --> B["ビルドの結果<br>だけ取り出す"] B --> C["動かす段階<br/>実行に必要な<br>ものだけ入れる"]
前 後
イメージの大きさ 1150MB 483MB
コンパイラ・ダウンロード用のツール あり なし

毎週のパッチの当て直しは、ビルドの段階がキャッシュされるので、今まで通り1分くらいで終わります。

6. その他の細かいもの

  • ログに残す内容を見直して、残す必要のない情報を出さないようにした
  • メールを送るときに、相手のサーバーの証明書を確かめるようにした(使っている標準の機能は、既定のままだと証明書を確かめません。知りませんでした)
  • エラーの内容を画面に返さず、ログにだけ残すようにした
  • パスワードを変えたときに、ほかの端末のログインも切れるようにした
  • ログイン後の画面を、ブラウザにキャッシュさせないようにした

おまけで、自分が記事を見ても PV が増えていたので、ログイン中は数えないようにしました。

確かめ方

直したら、毎回決まった手順で診断しています。

flowchart LR A["直す"] --> B["ローカルで動作確認"] B --> C["静的診断<br/>静的解析・依存<br/>パッケージの<br/>脆弱性チェック"] C --> D["動的診断<br/>隔離した環境で<br/>OWASP ZAP"] D --> E["本番へ反映"]

動的診断は、インターネットにも本番にもつながらない、隔離した Docker のネットワークの中だけで行っています。本番に疑似攻撃は送りません。

今回は3回通して、すべて合格でした。

つまずいたところ: パスワードに「$」を入れたら一部が消えた

パスワードを変えて docker compose up したら、こんな警告が出ました。

WARN[0000] The "abc" variable is not set. Defaulting to a blank string.

(表示の文字は、実際のものから変えています)

docker compose は、env_file に書いた値の中の $abc を「変数」として読んで、空の文字に置き換えてしまいます。パスワードの一部が消えて、設定と合わなくなります。

値をシングルクォートで囲むと、そのままの文字として扱われます。

EXAMPLE_VALUE='xxxx$abcxxxx'

ランダムなパスワードを作ると $ が入ることはよくあるので、注意してください。警告の文には、パスワードの一部がそのまま出てしまうので、人に見せるときも気をつけましょう。

追記: その後再度パスワードを変更しました。実は夜間バッチでコールドバックアップを取っているのですが、データベースのバックアップツールのエラーがどうしても解決できずに問題となる記号が入らないパスワードに変更してようやく解決しました。本番環境のパスワードなんてそうそう変えたくないです。お祭りになります(´;ω;`)ウッ…

AI に手伝ってもらった量(トークン数)

今回の監査と修正は、Claude Code(Anthropic の AI)と一緒にやりました。途中でモデルを切り替えています。

  • Claude Fable 5.2: ソースコードの監査と、レポートの作成
  • Claude Opus 5.5: 指摘の修正、検証、Docker のイメージの作り直し、この記事の草案

使ったトークン数は次の通りです(Claude Code の会話のログから集計。2026-11-11 23:37 時点)。

モデル API の呼び出し 入力(キャッシュ以外) キャッシュへの書き込み キャッシュからの読み込み 入力の合計 出力
Claude Fable 5.2 17回 36 267,555 3,158,561 3,426,152 43,464
Claude Opus 5.5 107回 246 445,119 40,618,523 41,063,888 117,302
合計 124回 282 712,674 43,777,084 44,490,040 160,766

入力のほとんどが「キャッシュからの読み込み」なのは、呼び出しのたびに、それまでの会話をまるごと読み直しているからです。会話が長くなるほど、1回あたりの入力が増えていきます。キャッシュからの読み込みは通常の入力より安く計算されるので、実際の料金は数字の見た目ほどにはなりません。

まとめ

  • 自作だと、フレームワークが黙って守ってくれていたものを、全部自分で用意する必要があります。「致命的なものは無い」でも、守りが1枚だけのところは多いので、2枚目を足すのが大事です
  • CSP は、先に「報告だけ」で入れて、自分のコードで止まるところをつぶしてから効かせると安全です
  • 直した後に、毎回同じ手順で確かめられるようにしておくと楽です

個人のブログでも、公開した瞬間から攻撃は毎日届きます(このブログのセキュリティ日報を見るとよく分かります)。少しずつでも固めていきましょう。

余談

Proプランでこの対応はやってみたのですが、あっというまにこんなことになりました。

Fable5.2怖い・・・物凄い勢いで減るのでご注意を!