安全加固:统一浏览器响应防护边界
This commit is contained in:
1 parent
836486cfd8
commit
3d6d4deabb
2 files changed
+77
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}));
|
||||
Reference in new issue
Block a user