ããã®ããŒã«ã¯ãã¡ã€ã«ããµãŒããŒã«éããŸããããšæžããŠãã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/ ãžã³ããŒãããŸãã
/*
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> ãåã蟌ãŸããŸããã
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ã®ã¬ã¹ãã³ã¹ã«æ³šå
¥ããããšã«ããŸããã
// æ¬çªãšåã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ã«ãŸãšããŠããŸãã
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ã€ã§ãã
- èªãµã€ããžã®ãªã¯ãšã¹ããæšªåãããHTMLã«ã ãCSPããããŒãè¶³ãïŒæ¬çªã®åçŸïŒ
-
ã³ã³ããã¹ãå
ã®å
šãªã¯ãšã¹ãã®URLãèšé²ããïŒèªãµã€ããš
blob:data:以å€ãããã°å€±æïŒ - ã³ã³ãœãŒã«ã«åºãCSPéåãéããïŒå³ããããŠäœããå£ããŠããã°å€±æïŒ
2ã¯ãéã£ãŠããªããã®ç¢ºèªã3ã¯ãCSPã®ããã§éå ·ãå£ããŠããªããã®ç¢ºèªã§ããçæ¹ã ãã ãšãããçæ¹ã®äºæ ãèŠéããŸãã
ã©ã®ãã¹ãã§ãèªåã§æ€æ»ãèµ°ãããã«ãã
ãã®fixtureã¯ã䜿ãå¿ãããšæå³ããããŸãããããã§ auto: true ã®fixtureã§å
ã¿ããã®ãã¡ã€ã«ã® test ã䜿ãã°å¿
ãæ€æ»ãèµ°ã ããã«ããŸããã
// requestsã䜿ããªããã¹ãã§ãæ€æ»ãèµ°ããããèªåã§æå¹ã«ãã
export const toolTest = test.extend<{ guard: void }>({
guard: [async ({ requests }, use) => use(void requests), { auto: true }],
});
éå
·ããšã®ãã¹ãïŒåçãå
¥ããŠä¿åãããæç« ãäŒãåã«ãããªã©ïŒã¯ããã¹ãŠãã® toolTest ããäœããŸãããããšãéå
·ãæäœããŠããæäžã«å€éšéä¿¡ã1ä»¶ã§ãèµ·ããã°ããã®ãã¹ããèœã¡ãŸãããããŒãžãéããç¬éãã ãã§ãªããããã¡ã€ã«ãå
¥ããŠåŠçããŠããæäžããæ€æ»ã§ããã®ã倧äºãªãšããã§ãã
ããŒãžãéãã ãã®æ€æ»ããå šéå ·ããŒãžã«å¯ŸããŠåããŠããŸãã
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ã«æž¡ããèŠä»¶å®çŸ©æžãã察話ã§äœãïŒã
åè
- Headers · Cloudflare Workers docs
- script-src - MDN
- frame-ancestors - MDN
- CSP navigate-to Directive
- Network - Playwright
- Fixtures - Playwright
AIã®å©çšã«ã€ããŠ
ãã®èšäºã¯ãClaudeã«äžæ¬¡çš¿ãäœæããŠããããå 容ã確èªã»ä¿®æ£ãããã®ã§ãããŠããšã®å®è£ ã¯Claude Codeãè¡ããç§ã¯ä»æ§ïŒåãå ¥ãæ¡ä»¶ïŒã決ããŠçµæã確ãããåŽã«åã£ãŠããŸããèšäºäžã®æè¡çãªèšè¿°ã¯ããã«ãã»ãã¹ãã»æ¬çªã®ããããŒã§ç¢ºèªããŸããã