安全加固:统一浏览器响应防护边界

This commit is contained in:
cieanyau committed 2026-11-07 03:54:49 +08:00
1 parent 836486cfd8
commit 3d6d4deabb
2 files changed
+77

No files matched your search

+12
View File
@@ -14,6 +14,18 @@ export default defineNuxtConfig({
// 恢复的源码使用显式 import,避免自动扫描带来同名组件或隐式依赖。 // 恢复的源码使用显式 import,避免自动扫描带来同名组件或隐式依赖。
components: false, components: false,
imports: { autoImport: 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: [ css: [
// 只放全站共享样式;各业务组件显式导入自己的带作用域样式。 // 只放全站共享样式;各业务组件显式导入自己的带作用域样式。
"~/assets/styles/entry.Cs9kvuGi.css", "~/assets/styles/entry.Cs9kvuGi.css",
+65
View File
@@ -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: '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>',
});
});
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,
};
}));