From 3d6d4deabb13be25e44673cb549dadfe921feb19 Mon Sep 17 00:00:00 2001 From: CieanYau Date: Sat, 7 Nov 2026 03:54:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=89=E5=85=A8=E5=8A=A0=E5=9B=BA=EF=BC=9A?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=B5=8F=E8=A7=88=E5=99=A8=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E9=98=B2=E6=8A=A4=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nuxt.config.ts | 12 +++++ tests/review-security-policy.e2e.test.mjs | 65 +++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 tests/review-security-policy.e2e.test.mjs diff --git a/nuxt.config.ts b/nuxt.config.ts index 34823b6..79b1833 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -14,6 +14,18 @@ export default defineNuxtConfig({ // 恢复的源码使用显式 import,避免自动扫描带来同名组件或隐式依赖。 components: false, imports: { autoImport: false }, + // 由响应入口统一设置浏览器信任边界;不限制现有验证码、支付和远控的脚本/连接来源。 + routeRules: { + "/**": { + headers: { + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "no-referrer", + "X-Frame-Options": "SAMEORIGIN", + "Content-Security-Policy": + "frame-ancestors 'self'; object-src 'none'; base-uri 'self'", + }, + }, + }, css: [ // 只放全站共享样式;各业务组件显式导入自己的带作用域样式。 "~/assets/styles/entry.Cs9kvuGi.css", diff --git a/tests/review-security-policy.e2e.test.mjs b/tests/review-security-policy.e2e.test.mjs new file mode 100644 index 0000000..9bec458 --- /dev/null +++ b/tests/review-security-policy.e2e.test.mjs @@ -0,0 +1,65 @@ +/** 浏览器防护由真实应用响应提供;不读取配置源码,也不接触真实凭证。 */ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { + appOrigin, + backendOrigin, + scenario, +} from "./helpers/console-browser.mjs"; + +test("Application routes consistently provide browser trust boundaries", (t) => + scenario(t, {}, async (page) => { + await page.goto(`${appOrigin}/login`); + const routes = ["/", "/login", "/account/realname", "/security/ssh-keys"]; + for (const path of routes) { + const response = await fetch(`${appOrigin}${path}`); + assert.equal(response.status, 200); + assert.equal(response.headers.get("x-content-type-options"), "nosniff"); + assert.equal(response.headers.get("referrer-policy"), "no-referrer"); + assert.equal(response.headers.get("x-frame-options"), "SAMEORIGIN"); + const policy = new Map( + (response.headers.get("content-security-policy") || "") + .split(";") + .map((directive) => directive.trim().split(/\s+/)) + .filter(([name]) => name) + .map(([name, ...values]) => [name, values]), + ); + assert.deepEqual(policy.get("frame-ancestors"), ["'self'"]); + assert.deepEqual(policy.get("object-src"), ["'none'"]); + assert.deepEqual(policy.get("base-uri"), ["'self'"]); + } + return { routes, consistentFramingMimeAndReferrerDefenses: true }; + })); + +test("Application navigation sends no referrer to resource servers", (t) => + scenario(t, {}, async (page) => { + await page.goto(`${appOrigin}/login?return_hint=private-test-marker`); + for (const origin of [appOrigin, backendOrigin]) { + const pixel = `${origin}/privacy-pixel.svg`; + let observe; + const seen = new Promise((resolve) => (observe = resolve)); + await page.context().route(pixel, async (route) => { + const headers = await route.request().allHeaders(); + observe(Boolean(headers.referer)); + await route.fulfill({ + contentType: "image/svg+xml", + body: '', + }); + }); + await page.evaluate((src) => { + const resource = document.createElement("img"); + resource.alt = ""; + resource.src = src; + document.body.append(resource); + }, pixel); + assert.equal( + await seen, + false, + "Resource requests must receive no referrer", + ); + } + return { + sameAndCrossOriginResourcesChecked: true, + referrerDisclosed: false, + }; + }));