一次真实项目中的安全加固实战经验分享
项目初期的技术选型
这个项目是个老后台系统,一开始就是个简单的 Vue + Element UI 搭的管理页,用户量不大,但最近被安全团队扫出一堆问题:XSS、CSRF、敏感信息泄露……直接被打了个中高危。负责人找到我说“得加固一下”,我第一反应是:这种小项目还能有啥大问题?结果一查审计报告,好家伙,光是未过滤的 innerHTML 就有七八处。
我们没时间重构整套权限体系,所以目标很明确:在不改架构的前提下,快速堵住已知漏洞。最后决定从三块入手:输入输出过滤、接口防护、资源访问控制。核心方案是:CSP + 输入 sanitizer + token 双校验。CSP 是重头戏,也是后来最让我头疼的部分。
最大的坑:CSP 引入后页面直接白屏
我一开始挺乐观,觉得 CSP 加个 header 就完事了。于是在 Nginx 配置里加了:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';";
本地一测,页面直接白屏。F12 看了一堆报错:
Refused to load script from 'https://jztheme.com/static/js/chunk-vendors.js' because it violates the following Content Security Policy directive: "script-src 'self'"
哦对,部署时静态资源走的是 CDN,域名和主站不一样,’self’ 不包括 CDN。改!
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://jztheme.com; style-src 'self' https://jztheme.com 'unsafe-inline'; img-src 'self' https://jztheme.com;";
这下能加载了,但控制台还是报警:
Refused to execute inline script because it violates CSP
翻代码发现,项目里有几个地方用了内联 script,比如动态生成图表配置的那段逻辑。这种 inline 脚本必须干掉,要么提出来,要么加 nonce。
提出来工作量太大,临时方案是加 nonce。思路是:服务端生成一个随机字符串,注入到 HTML 和 CSP 头里,只有带这个 nonce 的 script 才能执行。
Node.js 服务端代码改了一下:
const uuid = require('uuid');
app.get('/index.html', (req, res) => {
const nonce = uuid.v4().replace(/-/g, '');
const cspHeader =
default-src 'self';
script-src 'self' https://jztheme.com 'nonce-${nonce}';
style-src 'self' https://jztheme.com 'unsafe-inline';
img-src 'self' https://jztheme.com data:;
connect-src 'self' https://jztheme.com;
font-src 'self' https://jztheme.com;
object-src 'none';
base-uri 'self';
.replace(/s+/g, ' ').trim();
res.set('Content-Security-Policy', cspHeader);
const html = fs.readFileSync('./public/index.html', 'utf-8');
const injectedHtml = html.replace(
/<script/g,
<script nonce="${nonce}"
);
res.send(injectedHtml);
});
前端所有 inline script 都加上 nonce 属性:
<script nonce="abc123def">
// 初始化配置
window.APP_CONFIG = { api: '/api', env: 'prod' };
</script>
这下终于不报错了。但很快发现新问题:Webpack 打包出来的代码里,有些 loader 会动态插入 style 标签,这些标签没有 nonce,又被拦了。
最后妥协:保留 'unsafe-inline' 在 style-src,但加了 hash 限制。用 Webpack 构建时算出关键 CSS 的 sha256,硬编码进 CSP:
style-src 'self' https://jztheme.com 'unsafe-inline' 'sha256-abcdefg12345...';
虽然不够完美,但至少比全开 unsafe-inline 强。这里注意我踩过好几次坑:hash 必须是 base64 编码的二进制摘要,不是 hex 字符串,搞反了等于没加。
输入过滤的土办法
XSS 还有个重灾区是富文本展示。系统里有些日志详情页,直接把后端返回的 HTML 用 v-html 渲染,典型炸弹操作。
正经做法该上 DOMPurify,但我试了下发现它和我们用的某些自定义标签不兼容,净化完直接丢内容。时间紧,最后上了个“土方案”:自己写了个简易 sanitizer,只放行基本标签和属性:
function sanitize(dirty) {
const div = document.createElement('div');
div.innerHTML = dirty;
const allowedTags = ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'code'];
const allowedAttrs = ['class'];
function walk(node) {
for (let i = node.childNodes.length - 1; i >= 0; i--) {
const child = node.childNodes[i];
if (child.nodeType === 1) { // element
if (!allowedTags.includes(child.tagName.toLowerCase())) {
child.remove();
continue;
}
// 过滤属性
const attrs = [...child.attributes];
attrs.forEach(attr => {
if (!allowedAttrs.includes(attr.name)) {
child.removeAttribute(attr.name);
}
});
walk(child);
}
}
}
walk(div);
return div.innerHTML;
}
然后在组件里这样用:
<div v-html="sanitize(logContent)"></div>
亲测能防住大部分 和 onxxx 事件注入。当然,遇到 svg 或 mathML 还是有风险,不过我们业务场景压根不用这些,暂时不管了。
接口防刷和 CSRF 的双保险
接口这块,原本只有 JWT 鉴权,但安全报告说容易被暴力试探。我们加了两层:一是限流,用 Redis 记录用户 IP + 接口路径的请求频次;二是敏感操作加二次 token 校验。
比如删除接口,前端不仅要带 JWT,还得从页面 meta 拿一个一次性 token:
<meta name="csrf-token" content="abc123xyz">
请求时塞进 header:
“javascript
axios.delete('/api/item/123', {
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
}
});
>csrf:abc123xyz
<p>后端对比 token 是否匹配且未使用过。用 Redis 存 ,值设为 used,TTL 5 分钟。虽然增加了复杂度,但至少防住了简单 CSRF 和自动化脚本批量提交。</p>
<h2>回顾与反思</h2>
<p>上线两周,安全扫描通过了,也没收到用户投诉。但从开发者角度看,这方案真不算优雅。CSP 的 nonce 注入是侵入式的,每次发布都得保证服务端能动态注入,要是哪天换成了纯静态部署,这套就废了。理想情况应该是构建时生成 nonce 占位符,但我们现在用的传统打包流程,改不动。</p>
<p>另外 sanitizer 那块其实还有隐患:如果后端返回的内容里有 `,虽然我在 JS 里过滤了 onerror,但如果 HTML 字符串拼接时顺序不对,可能漏掉。后来补了条规则:移除所有包含 onw+= 的属性名,算是勉强兜住。
最大的遗憾是没上 report-only 模式做灰度。本来计划先上 CSP-Report-Only 收集异常,结果测试环境没法复现生产的数据流,上报日志全是空的,最后只能生产直接上,提心吊胆半天。
总的来说,这次加固像是打了一排补丁,但确实解决了眼前问题。有些地方没做到 100% 安全,但在当前人力和周期下,已经是最优解了。至少现在半夜不会被安全告警电话吵醒了。
以上是我的项目经验,希望对你有帮助
这个技巧的拓展用法还有很多,比如 CSP 结合 SRI 做资源完整性校验,或者用 Trusted Types 做更深层防御,但对我们项目来说太重了。后续如果有新项目,我会考虑从一开始就集成这类机制,而不是等出事再补。
以上是我踩坑后的总结,有更优的实现方式欢迎评论区交流。

暂无评论