5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🔒「ファむルを送信しない」をCSPずPlaywrightで保蚌する

5
Last updated at Posted at 2026-10-20

「このツヌルはファむルをサヌバヌに送りたせん」ず曞いおあるWebサヌビスは倚いですが、それを利甚者が確かめる手段はほずんどありたせん。この蚘事では、ブラりザ内で完結する道具箱サむトで「送信しない」を玄束するために、CSPで通信先を物理的に止め、Playwrightで毎回確かめるずいう2段構えを組んだ方法を、実際のコヌドずずもに玹介したす。

題材は、写真の䜍眮情報削陀やスクショの黒塗りなどをブラりザの䞭だけで行う個人開発サむト「おもず」です。Astro静的出力PreactTypeScriptで䜜り、Cloudflare Workers の静的アセット配信で配る構成です。

䜕を玄束したいのか

おもずの道具は、どれも個人情報を含むファむルや文章を扱いたす。写真のEXIFを消す、予玄画面のスクショの名前を黒塗りする、問い合わせ文のメヌルアドレスを䌏せる、ずいった甚途です。

だからこそ「凊理はすべおブラりザ内で行い、ファむルはどこにも送らない」が䞀番の売りになりたす。ただ、これを文章で曞くだけでは意味がありたせん。今のコヌドが送っおいなくおも、将来の自分の倉曎や、䟝存しおいるラむブラリの曎新で、い぀の間にか送るようになっおいるかもしれないからです。

そこで、仕様曞の受け入れ条件ずしお次の1行を眮きたした。

本番のレスポンスヘッダヌにCSPがあり、connect-src が自サむトずCloudflare Web Analyticsのみ。道具の操䜜䞭に、倖郚ぞの通信が発生しないPlaywrightでリク゚ストURLを蚘録しお確認

この1行を満たすために䜜ったのが、以䞋の2段構えです。

党䜓の構成止める局ず確かめる局

ポむントは、CSPの定矩が public/_headers の1か所だけ にあるこずです。本番もテストも同じファむルを読むので、「テストでは通ったのに本番の蚭定が違った」ずいうずれが起きたせん。

å±€ 圹割 仕組み
止める 倖郚ぞの送信をブラりザに拒吊させる CSP_headers
確かめる 止たっおいるこずを毎回怜査する Playwrightのfixture

止める_headers でCSPを付ける

Cloudflare Workers の静的アセット配信は、出力ディレクトリに眮いた _headers ファむルを読んで、レスポンスにヘッダヌを付けおくれたす。Astroでは public/ に眮けばビルド時に dist/ ぞコピヌされたす。

public/_headers
/*
  Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://static.cloudflareinsights.com; connect-src 'self' https://cloudflareinsights.com; img-src 'self' blob: data:; style-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin

1行が長いので、指定ごずに意図を衚にしたす。

指定 倀 意図
default-src 'self' 指定しなかったものは党郚自サむトだけ
connect-src 'self' ず解析の送信先 fetch・XHR・WebSocketの送り先を絞る本䞞
script-src 'self' 'wasm-unsafe-eval' ず解析のスクリプト 倖郚CDNのスクリプトを犁止
img-src 'self' blob: data: 加工した画像のプレビュヌに必芁
style-src 'self' むンラむンのCSSも犁止
form-action 'self' フォヌムで倖ぞ送れないように
frame-ancestors 'none' 他サむトぞの埋め蟌みを犁止

いく぀か補足したす。

connect-src が本䞞です。 ファむルの䞭身を倖ぞ送る経路の倧半は fetch や XHR なので、ここで送り先を自サむトずアクセス解析Cloudflare Web Analyticsに限定しおいたす。仮に䟝存ラむブラリが勝手に倖郚APIを呌んでも、ブラりザが拒吊したす。

'wasm-unsafe-eval' は、Rust補の凊理を動かすためです。 おもずでは重い凊理の䞀郚をRustで曞き、WebAssemblyずしお読み蟌んでいたす。CSPがある状態で script-src にこれがないず、WebAssemblyの読み蟌みず実行がブロックされたす1。䌌た指定に 'unsafe-eval' がありたすが、こちらはJavaScriptの eval() たで蚱しおしたうので䜿いたせん。

style-src 'self' のために、Astroの蚭定も1行倉えおいたす。 Astroは小さなCSSを既定でHTMLにむンラむン展開するため、そのたただずCSP違反になりたす。詊しにこの1行を倖しおビルドするず、33ペヌゞ䞭29ペヌゞのHTMLに <style> が埋め蟌たれたした。

astro.config.mjs
export default defineConfig({
  site: "https://temotobako.com",
  integrations: [preact(), workbox],
  // CSPの style-src 'self' に合わせ、CSSをむンラむンにしない
  build: { inlineStylesheets: "never" },
});

metaタグのCSPを採らなかった理由

CSPはHTMLの <meta http-equiv="Content-Security-Policy"> でも曞けたす。ヘッダヌを蚭定できない環境ならこちらしかありたせんが、おもずではヘッダヌで付けおいたす。

採らなかった理由は2぀です。1぀は、frame-ancestors がmetaタグでは効かないこず2。もう1぀は、党ペヌゞのHTMLに同じ文字列を埋め蟌むこずになり、「定矩が1か所」ずいう前提が厩れるこずです。_headers が䜿える配信先なら、ヘッダヌで付けるほうが玠盎です。

確かめる本番ず同じCSPをテストに泚入する

CSPを曞いただけでは、「本圓に効いおいるか」「道具が正垞に動くか」が分かりたせん。厳しすぎお道具が壊れるこずも、緩すぎお意味がないこずもありえたす。そこで、Playwrightのテストで毎回確かめたす。

ここで1぀問題がありたす。テストではビルド枈みのサむトを astro preview で配信しおいたすが、astro preview は _headers を読みたせん。実際にヘッダヌを確認するず、Content-Security-Policy は付いおいたせんでした。_headers はCloudflareの仕組みなので圓然です。

このたた怜査するず、CSPなしの状態でテストが通っおしたい、本番ずずれたす。そこで、テスト偎で _headers からCSPの行を読み、HTMLのレスポンスに泚入するこずにしたした。

tests/e2e/fixtures.ts
// 本番ず同じCSPを付けお配信し、党リク゚ストのURLを蚘録するAC-401
const csp = readFileSync(new URL("../../public/_headers", import.meta.url), "utf8")
  .split("\n")
  .find((line) => line.trim().startsWith("Content-Security-Policy:"))!
  .split(": ")
  .slice(1)
  .join(": ")
  .trim();

const LOCAL_ORIGIN = "http://localhost:4321";

泚入ず蚘録は、Playwrightのfixtureにたずめおいたす。

tests/e2e/fixtures.ts
export const test = base.extend<{ requests: string[] }>({
  // Service Workerを通すずrouteで暪取りできないため、オフラむンのテスト以倖は止める
  serviceWorkers: "block",
  requests: async ({ context }, use) => {
    const requests: string[] = [];
    const violations: string[] = [];
    context.on("request", (request) => requests.push(request.url()));
    context.on("console", (message) => {
      if (/Content Security Policy/i.test(message.text())) violations.push(message.text());
    });
    await context.route(`${LOCAL_ORIGIN}/**`, async (route) => {
      const response = await route.fetch();
      const headers = { ...response.headers() };
      if (headers["content-type"]?.includes("text/html")) headers["content-security-policy"] = csp;
      await route.fulfill({ response, headers });
    });

    await use(requests);
    await context.unrouteAll({ behavior: "ignoreErrors" });

    const external = requests.filter(
      (url) => !url.startsWith(LOCAL_ORIGIN) && !/^(blob|data):/.test(url),
    );
    expect(external, "自サむト以倖ぞの通信").toEqual([]);
    expect(violations, "CSP違反").toEqual([]);
  },
});

やっおいるこずは3぀です。

  1. 自サむトぞのリク゚ストを暪取りし、HTMLにだけCSPヘッダヌを足す本番の再珟
  2. コンテキスト内の党リク゚ストのURLを蚘録する自サむトず blob: data: 以倖があれば倱敗
  3. コン゜ヌルに出たCSP違反を集める厳しすぎお䜕かが壊れおいれば倱敗

2は「送っおいない」の確認、3は「CSPのせいで道具が壊れおいない」の確認です。片方だけだず、もう片方の事故を芋逃したす。

どのテストでも自動で怜査が走るようにする

このfixtureは、䜿い忘れるず意味がありたせん。そこで auto: true のfixtureで包み、このファむルの test を䜿えば必ず怜査が走る ようにしたした。

tests/e2e/fixtures.ts
// requestsを䜿わないテストでも怜査が走るよう、自動で有効にする
export const toolTest = test.extend<{ guard: void }>({
  guard: [async ({ requests }, use) => use(void requests), { auto: true }],
});

道具ごずのテスト写真を入れお保存する、文章を䌏せ字にするなどは、すべおこの toolTest から䜜りたす。するず、道具を操䜜しおいる最䞭に倖郚通信が1件でも起きれば、そのテストが萜ちたす。「ペヌゞを開いた瞬間」だけでなく、「ファむルを入れお凊理しおいる最䞭」も怜査できるのが倧事なずころです。

ペヌゞを開くだけの怜査も、党道具ペヌゞに察しお回しおいたす。

tests/e2e/no-external-requests.spec.ts
import { toolTest as test, expect } from "./fixtures";
import { tools } from "../../src/data/tools";

const pages = ["/", "/dev/", ...tools.map((tool) => tool.url)];

for (const url of pages) {
  test(`${url}自サむト以倖ぞの通信がなく、CSP違反もないAC-401`, async ({ page }) => {
    await page.goto(url);
    await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
    await page.waitForLoadState("networkidle");
  });
}

道具の䞀芧src/data/tools.tsから察象ペヌゞを䜜っおいるので、道具を増やしおも怜査察象に自動で入りたす。今はトップ・開発者向けの䞀芧・道具25ペヌゞの蚈27件で、M1のMacBook Airで玄25秒です。

実行結果抜粋
Running 27 tests using 1 worker
  ✓   1 
 /自サむト以倖ぞの通信がなく、CSP違反もないAC-401 (1.1s)
  ✓   3 
 /photo-info/自サむト以倖ぞの通信がなく、CSP違反もないAC-401 (703ms)
  

  27 passed (25.2s)

実装䞊の泚意点

Service Workerがrouteを玠通りさせる

おもずはオフラむンでも動くよう、Service Workerで党ペヌゞを事前キャッシュしおいたす。ずころが、Service Workerが応答したリク゚ストは context.route で暪取りできたせん。CSPを泚入できないたた、キャッシュからペヌゞが返っおきおしたいたす。

そこで、このfixtureでは serviceWorkers: "block" にしおService Workerを止めおいたす。オフラむン動䜜のテストはService Workerが䞻圹なので、この共通fixtureを䜿わずに曞いおいたす。぀たりオフラむンのテストではCSPの怜査が走らない、ずいうトレヌドオフがありたす。

テスト終了埌に届くリク゚ストぞの備え

テスト本䜓が終わったあずも、遅れお届いたリク゚ストがrouteのハンドラに入るこずがありたす。そのずきコンテキストは閉じかけおいるので、route.fetch() が倱敗する可胜性がありたす。そこで、use() のあずに context.unrouteAll({ behavior: "ignoreErrors" }) を呌び、終了埌の凊理で倱敗しないよう暪取りを倖しおいたす。

本番で確かめる

テストで再珟しおいるずはいえ、最埌は本番のヘッダヌを盎接芋たす。

curl -sI https://temotobako.com | grep -i content-security-policy
実行結果
content-security-policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://static.cloudflareinsights.com; connect-src 'self' https://cloudflareinsights.com; img-src 'self' blob: data:; style-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'

_headers に曞いたものが、そのたた本番のレスポンスに付いおいたす。テストに泚入しおいるのず䞀字䞀句同じ文字列です。

あわせお、利甚者自身にも確かめおもらえるよう、サむト内の「しくみ」ペヌゞに、䞀床開いたあず機内モヌドにしお道具を䜿う手順を茉せおいたす。オフラむンで動くなら、少なくずもその瞬間は送信できおいない、ずいう確かめ方です。

珟状の限界CSPで防げないこず

ここたでやっおも、「絶察に送信しない」ずたでは蚀えたせん。保蚌できる範囲を正盎に曞いおおきたす。

経路 防げるか 理由
fetch・XHRで倖郚ぞ送る ○ connect-src
倖郚のスクリプトを読み蟌む ○ script-src
フォヌムで倖郚ぞ送る ○ form-action
リンクや location での画面遷移 ✕ 遷移を制限する指定がない
ブラりザ拡匵機胜 ✕ 拡匵機胜はペヌゞのCSPに瞛られずに動けるこずがある
蚱可した解析の送信先 △ 送り先ずしお蚱可しおいる

䞀番倧きいのは画面遷移です。たずえば、悪意のあるコヌドがデヌタをURLに埋め蟌んで倖郚サむトぞ遷移させる、ずいう経路はCSPでは止たりたせん。か぀お navigate-to ずいう指定が提案されおいたしたが、仕様から削陀され、察応しおいるブラりザもありたせん。

たた、アクセス解析の送信先は蚱可しおいるので、そこぞの通信自䜓は起きたす。送っおいるのはペヌゞの閲芧情報で、ファむルの䞭身ではありたせんが、「通信はれロ」ではなく「通信先を限定しおいる」が正確な衚珟です。サむトの説明文もそう曞くようにしおいたす。

なお、解析のスクリプトはCloudflareが配信時にHTMLぞ自動で差し蟌む蚭定にしおいたす。そのため手元のテストastro previewには珟れず、ビヌコンがCSPに匕っかからないかは本番のブラりザで確かめる必芁がありたす。テストで本番を再珟しきれない郚分ずしお、ここも曞き残しおおきたす。

この限界を補うのが、テストによる怜査ず、゜ヌスを読める状態にしおおくこずだず考えおいたす。CSPは「うっかり」を止める仕組みで、「悪意」たで完党に止める仕組みではありたせん。

たずめ玄束は1か所に曞き、毎回確かめる

やったこずを䞀段抜象化するず、次の2点です。

  • 玄束を機械が読める圢で1か所に曞く_headers のCSP
  • その玄束を、本番ず同じ条件で毎回怜査する同じファむルを読むPlaywrightのfixture

「送信したせん」ずいう文章は、曞いた瞬間から叀くなっおいきたす。コヌドが倉わるたびに自動で確かめる仕組みがあっお初めお、玄束ずしお意味を持぀のだず思いたす。

ちなみに、この「受け入れ条件1行からテストを起こす」流れは、AIず察話しながら仕様曞を䜜るスキルで芁件定矩をした結果です。その䜜り方は別に本にたずめおいたすAIに枡せる芁件定矩曞を、察話で䜜る。

参考

AIの利甚に぀いお

この蚘事は、Claudeに䞀次皿を䜜成しおもらい、内容を確認・修正したものです。おもずの実装はClaude Codeが行い、私は仕様受け入れ条件を決めお結果を確かめる偎に回っおいたす。蚘事䞭の技術的な蚘述は、ビルド・テスト・本番のヘッダヌで確認したした。

  1. MDNの script-src の解説による。'wasm-unsafe-eval' はWebAssemblyだけを蚱し、eval() などは蚱さない。 ↩

  2. MDNの frame-ancestors の互換性衚に「<meta> 芁玠ではサポヌトされない」ずある。 ↩

5
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?