Web開発
私の静的サイトが内部ドキュメントを配信していました
My static site was serving my internal docs (tminuslabs.space)
要約
著者は、静的ホスティングプラットフォームの設定ミスにより、プロジェクトの内部ドキュメント(管理用クエリパラメータやスキーマの脆弱性に関する情報を含む)が公開されていたインシデントを報告しています。このインシデントは、ファイルがリポジトリのルートにあり、ホスティングサービスがドットファイルを含むすべてのファイルを公開したために発生しました。修正には、CDNキャッシュのパージ、ファイアウォールルールの適用、およびサーバーサイドの脆弱性の修正が必要でした。
全文翻訳
公開時点での状況
以下は、2026年8月30〜31日頃の状況を説明しています。それ以降:漏洩したファイルは削除され、ファイアウォールルールによってエッジでブロックされるようになりました。管理クエリ文字列値はローテーションされ、実際に重要な修正として、ビジネスを「アクティブ」とマークできるが支払いを行わないようにするスキーマの穴がサーバーサイドで閉じられ、本番環境で検証されました。管理パラメータは、どちらの点でももはや機能するバイパスではありません。
これはライブシステムのマッピングではなく、クローズされたインシデントのポストモーテムです。
私は習慣で、リポジトリの1つにあるMarkdownファイルに戦略メモをコミットしようとしていました。それを行う前に、ファイルがWebから到達可能かどうかを確認するために立ち止まりました。
$ curl -s -o /dev/null -w "%{http_code} %{size_download}\n" \
https://loyalty.tminuslabs.space/HANDOFF.md
200 150205
200、そして150キロバイト。それは私のエンジニアリングハンドオフドキュメント、つまりプロジェクト全体の進行中のメモであり、それを求めたすべての人に配信されていました。
何が含まれていたか
資格情報として適格なものはありません。APIキー、トークン、顧客データはありません。これだけが、このブログ投稿がもっと悪い週ではなく、ブログ投稿である理由です。
含まれていたもの:
オンボーディング中に支払いステップをスキップする管理クエリ文字列パラメータ。チェックアウトをスキップするパスとして、11回、それぞれ明確に説明されていました。
私自身のスキーマにおける既知の未修正の穴の記述。クライアントが挿入時に設定できる列で、誰でも支払いが完了したように見えるアカウントを支払わずに作成できます。自分へのメモとして、再現として記述されていました。
データベース識別子、すべてのテーブルと関数名、およびどの権限がいつ強化されたかの完全な履歴。
したがって、データ侵害ではありません。取扱説明書です。
なぜ公開されていたか
静的ホストはビルド出力ディレクトリを公開します。このプロジェクトでは、そのディレクトリはリポジトリのルートでした。したがって、リポジトリ内の追跡されているすべてのファイルは公開URLであり、ハンドオフドキュメントはドキュメントが属する場所であるリポジトリ内にありました。
メンタルモデルの失敗は単純で、おそらく一般的です。私はリポジトリを「プロジェクト」と、サイトを「その中のページ」と考えていました。ホストはその区別をしません。それはディレクトリ内のものをアップロードします。
ドットディレクトリは免除されません
ドットで始まるものはすべてスキップされると想定していました。そうではありません。
.claude/launch.json、エディタツールの設定が200で配信され、私のユーザー名を含むローカルファイルシステムのパスを漏洩しました。兄弟プロジェクトでは、.github/workflows/*.ymlと.github/scripts/*.jsも同様でした。一方、.DS_Storeは配信されませんでした。その理由は、1行で全体のメカニズムです。それは.gitignoreにあり、コミットされたことがなかったため、アップロードされたこともありませんでした。このホストでは、公開は、そのディレクトリ内にコミットされたものを正確に公開し、ファイル名で何もブロックしません。これは特定のプラットフォームに関する発見であり、静的ホスティングの法則ではありません。一部のホスト(GitHub Pages、Netlify)はデフォルトでドットファイルをスキップします。どちらかの方法で想定するのではなく、お使いのホストの実際の動作を確認してください。
機能しなかった修正、2回
ファイルを削除して再デプロイするのは簡単でした。それが確認できなかった。デプロイ後もファイルは配信されていました。CDNキャッシュをパージしました。まだ配信されていました。もう一度パージしました。まだ配信されていました。
ヘッダーを正しく読んだ後、それらが説明しました。スキミングするのではなく:
cf-cache-status: DYNAMIC
age: 67735
cache-control: public, s-maxage=604800
cf-cache-status: DYNAMIC は、プラットフォームのゾーンレベルキャッシュがこのキャッシュからまったく配信されなかったことを意味します。「すべてをパージ」は、それが決してそこにないため、決してそれに触れることはありませんでした。ageとs-maxage=604800は、まったく別のキャッシュレイヤーに属しています。ゾーンキャッシュの前面にある静的ホスト自体の資産/エッジティアであり、ゾーンパージはそこに到達しません。ageはチェックごとに着実に増加しました(4,396、次に10,139、次に67,735)。壁時計時間を追跡しており、パージがまったく触れなかったオブジェクトから期待されることとまったく同じです。パージが実行されて失敗したのではなく。
決定的な証拠は、同じデプロイメントを配信する2つのホスト名を比較することでした。プラットフォーム固有のデフォルトドメインは、クリーンな現在のバージョンを返しました。カスタムドメインは古いファイルを返しました。同じ基盤となるデプロイメント、異なる回答。このホスト名ごとの分割は、キャッシュステータスヘッダーだけでは、古いコピーが実際にどこにあったかを示すものではありません。
ヘッダーだけを読むと、「Cloudflareは私のパージを無視した」ように見えます。それは何も無視しませんでした。パージツールと古いコピーを保持しているレイヤーは2つの異なるシステムでした。
2つのケース「デプロイに失敗した」と「デプロイは機能し、キャッシュは古い」を分離する診断は、外部からは同じように見えます。ここから見分ける方法です。
キャッシュキーには、クエリ文字列を含む完全なURLが含まれます。したがって、一度も要求されたことのないURLはどのキャッシュにも存在できず、オリジンにアクセスする必要があります。
curl -s -o /dev/null -w "%{size_download}\n" \
"https://your-site.com/notes.md?cb=$RANDOM"
これが404フォールバックを返し、プレーンURLがファイル自体を返す場合、デプロイは正常であり、キャッシュの問題があります。両方がファイル自体を返す場合、デプロイはあなたが思ったようには機能しませんでした。
落とし穴 — 私は2回落ちました
そのトリックを発見した後、私は修正を確認するためにそれを使用しました。それはオリジンがクリーンであることを私に伝え、私は問題を解決したと宣言し、先に進みました。2回。しかし、キャッシュをバイパスしたリクエストは「オリジンはクリーンか」という質問に答えますが、「訪問者はまだこれを取得できるか」という質問とは異なります。実際の訪問者はプレーンURLを送信し、キャッシュにヒットします。私の検証は、疑いの余地がなかった唯一のものを測定していました。
プレーンリクエストで公開を確認してください。診断にはキャッシュバイパスを使用しますが、修正の確認には絶対に使用しないでください。
実際にクローズしたもの
影響を受けるホスト名にスコープされたファイアウォールルール。漏洩したファイルに固有のいくつかのパス文字列に一致します。これらはキャッシュが consultar される前にエッジで実行されるため、キャッシュがどこにキャッシュされているかに関係なく機能し、7日間のTTLを待つのではなく数秒で効果を発揮します。
(http.host eq "your-production-host" and ( http.request.uri.path contains "<leaked-doc-token>" or http.request.uri.path contains "/.claude/" or http.request.uri.path contains "<tooling-file-token>" )) → Block
1つの詳細が余分な往復を招きました。私の最初の試みは、リテラルスペースを含むトークンに一致しました。パスはワイヤー上でパーセントエンコードされます。スペースは%20になります。そのため、一致しませんでした。スペースのないトークンに一致させてください。
最も長く認めるのにかかった部分
公開をクローズした後、明らかな次のステップは、ドキュメントが明らかにした管理文字列をローテーションすることでした。それで、私はそうしました。次に、それが存在した公開ページを確認しました。
$ curl -s https://loyalty.tminuslabs.space/onboarding | grep -c "<the old value>"
7
(意図的にここで編集済み — 実際の数字は重要ではなく、実際のバイパス値を公開の書き込みにそのまま再現することは、それが死んだ後でも繰り返す価値のある詳細です。)
7回のヒット。世界中が読めるファイルにあります。チェックはクライアントサイドでした。値は常にページソースにありました。漏洩したドキュメントは決して公開されたわけではありません。view-sourceはそうでした。そして、私がそこに置いたどんな値であっても、それは常にそうでした。したがって、ローテーションはほとんど何も購入しませんでした。それはいくつかの古いキャッシュされたコピーに書き込まれた文字列を無効にしましたが、それが本当にすべてでした。本当の修正は、ドキュメントが説明したサーバーサイドの穴でした。クライアントが決して設定できなかった列。それを閉じることで、管理パラメータは、誰がそれを知っていても、侵入方法として停止しました。
一般化できる部分
秘密が漏洩すると、それをローテーションするのが本能です。最初に、漏洩したものが本当に秘密だったのかどうかを尋ねる価値があります。クライアントサイドコードに存在するなら、それは漏洩前に公開されており、ローテーション後も公開されます。そして、それをローテーションすることは、主にあなたが行動したという感覚を得るだけです。
短いチェックリスト
あなた自身のリポジトリの非ページファイルを、あなたの本番ドメインに対して取得してください。README.md、.env、.git/config、ソースマップ、エディタ設定、CIファイル。プレーンなリクエストを使用してください。