安全加固:统一浏览器响应防护边界
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,避免自动扫描带来同名组件或隐式依赖。
|
// 恢复的源码使用显式 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",
|
||||||
|
|||||||
@@ -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