From 01a082c8da0243a82382d476dd13dee53c610e9d Mon Sep 17 00:00:00 2001 From: whgojp <3242246891@qq.com> Date: Sun, 24 May 2026 17:45:35 +0800 Subject: [PATCH 01/19] docs: improve xss module guidance --- .../modules/xss/controller/DomController.java | 2 +- .../xss/controller/OtherController.java | 5 +- .../xss/controller/ReflectController.java | 22 +-- .../xss/controller/StoreController.java | 2 +- src/main/resources/static/js/staticcode.js | 62 +++++-- .../resources/templates/vul/xss/dom-href.html | 6 +- src/main/resources/templates/vul/xss/dom.html | 160 +++++++++++++++++- .../resources/templates/vul/xss/other.html | 22 ++- .../templates/vul/xss/reflect/safe.html | 42 +++-- .../templates/vul/xss/reflect/vul.html | 10 +- .../resources/templates/vul/xss/store.html | 21 ++- 11 files changed, 273 insertions(+), 81 deletions(-) diff --git a/src/main/java/top/whgojp/modules/xss/controller/DomController.java b/src/main/java/top/whgojp/modules/xss/controller/DomController.java index d745015..e5034c8 100644 --- a/src/main/java/top/whgojp/modules/xss/controller/DomController.java +++ b/src/main/java/top/whgojp/modules/xss/controller/DomController.java @@ -14,7 +14,7 @@ * @Date: 2024/5/23 17:25 */ @Slf4j -@Api(value = "ReflectController", tags = "跨站脚本-Dom型XSS") +@Api(value = "DomController", tags = "跨站脚本-DOM型XSS") @Controller @CrossOrigin(origins = "*") @RequestMapping("/xss/dom") diff --git a/src/main/java/top/whgojp/modules/xss/controller/OtherController.java b/src/main/java/top/whgojp/modules/xss/controller/OtherController.java index 727ce23..e79a44b 100644 --- a/src/main/java/top/whgojp/modules/xss/controller/OtherController.java +++ b/src/main/java/top/whgojp/modules/xss/controller/OtherController.java @@ -84,7 +84,7 @@ public R hackCookie(@RequestParam String cookie, HttpServletRequest request) { private UploadUtil uploadUtil; // 文件上传接口 - @ApiOperation(value = "漏洞场景:文件上传导致存储XSS", notes = "原生漏洞场景,未加任何过滤,Controller接口返回Json类型结果") + @ApiOperation(value = "漏洞场景:文件上传导致存储XSS", notes = "上传可被浏览器或预览服务解析的文件,后续访问文件时可能触发XSS") @RequestMapping("/vul1Upload") @ResponseBody @SneakyThrows @@ -110,6 +110,7 @@ public R vul1Upload(@RequestParam("file") MultipartFile file, } catch (Exception e) { return R.error("上传错误,请检查后重新上传:" + e.getMessage()); } + // XML解析成功后继续落盘,便于演示“解析 + 可访问文件”组合场景。 case "html": case "svg": case "pdf": @@ -122,7 +123,7 @@ public R vul1Upload(@RequestParam("file") MultipartFile file, return R.error(res); } } - @ApiOperation(value = "漏洞场景:模版引擎解析导致存储XSS", notes = "") + @ApiOperation(value = "漏洞场景:模板引擎不安全渲染导致XSS", notes = "th:utext会把内容作为HTML渲染,th:text会进行转义") @GetMapping("/vul2OtherTemplate") public String vul2OtherTemplate(@RequestParam("payload") String payload, @RequestParam("type") String type, Model model) { diff --git a/src/main/java/top/whgojp/modules/xss/controller/ReflectController.java b/src/main/java/top/whgojp/modules/xss/controller/ReflectController.java index 7df5caf..6266dd4 100644 --- a/src/main/java/top/whgojp/modules/xss/controller/ReflectController.java +++ b/src/main/java/top/whgojp/modules/xss/controller/ReflectController.java @@ -41,7 +41,7 @@ public String reflect(@PathVariable String view) { return isValidView(view) ? "vul/xss/reflect/" + view : "error/404"; } - @ApiOperation(value = "漏洞场景:GET型与POST型", notes = "原生漏洞场景,未加任何过滤,Controller接口返回Json类型结果") + @ApiOperation(value = "漏洞场景:GET型与POST型", notes = "原生漏洞场景,未加任何过滤,Controller接口返回JSON类型结果。JSON本身通常不会直接触发XSS,但前端不安全渲染JSON字段时可能触发") @RequestMapping("/vul1") @ResponseBody @ApiImplicitParam(name = "payload", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class) @@ -59,7 +59,7 @@ public String vul2(@ApiParam(name = "payload", value = "请求参数", required } @SneakyThrows - @ApiOperation(value = "漏洞场景:Content-Type问题", notes = "Tomcat内置HttpServletResponse,Content-Type导致反射XSS") + @ApiOperation(value = "漏洞场景:Content-Type问题", notes = "响应Content-Type决定浏览器解析方式,不可信内容以text/html返回时可能导致反射XSS") @GetMapping("/vul3") @ResponseBody @ApiImplicitParams({ @@ -77,7 +77,7 @@ public void vul3(@ApiParam(name = "type", value = "类型", required = true) @Re case "plain": log.info("[+]XSS-反射性-Content-Type:text/plain;charset=utf-8:" + payload); response.getWriter().print(payload); - response.setContentType("text/plain;charset=utf-8"); // response默认返回Content-Type类型是text/plain + response.setContentType("text/plain;charset=utf-8"); response.getWriter().flush(); break; default: @@ -91,7 +91,7 @@ public void vul3(@ApiParam(name = "type", value = "类型", required = true) @Re private static final String WHITELIST_REGEX = "^[a-zA-Z0-9_\\s]+$"; private static final Pattern pattern = Pattern.compile(WHITELIST_REGEX); - @ApiOperation(value = "安全代码:用户输入验证和过滤", notes = "对用户输入的数据进行验证和过滤,确保不包含恶意代码。使用白名单过滤,只允许特定类型的输入,如纯文本或指定格式的数据") + @ApiOperation(value = "安全代码:用户输入验证和过滤", notes = "使用白名单限制输入格式,适合约束字段类型;最终仍需根据输出位置进行上下文编码") @GetMapping("/safe1") @ResponseBody @ApiImplicitParams({ @@ -116,13 +116,13 @@ public R safe1(@ApiParam(name = "type", value = "类型", required = true) @Requ return R.ok(filterContented); } - @ApiOperation(value = "安全代码:内容安全策略-CSP防护", notes = "内容安全策略(Content Security Policy)是一种由浏览器实施的安全机制,旨在减少和防范跨站脚本攻击(XSS)等安全威胁。它通过允许网站管理员定义哪些内容来源是可信任的,从而防止恶意内容的加载和执行") + @ApiOperation(value = "安全代码:内容安全策略-CSP防护", notes = "内容安全策略(Content Security Policy)是由浏览器实施的额外防护层,可降低恶意脚本加载和执行风险,但不能替代输出编码与安全模板/DOM用法") @GetMapping("/safe2") @ResponseBody @ApiImplicitParam(name = "payload", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class) public String safe2(@ApiParam(name = "payload", value = "请求参数", required = true) @RequestParam String payload, HttpServletResponse response) { - response.setHeader("Content-Security-Policy", "default-src self"); - response.setHeader("Content-Security-Policy-Report-Only", "default-src 'self'; other-uri /xss/reflect/csp-other-endpoint"); + response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'"); + response.setHeader("Content-Security-Policy-Report-Only", "default-src 'self'; report-uri /xss/reflect/csp-report-endpoint"); log.info("[-]XSS-反射性-内容安全策略-CSP防护:" + payload); return payload; } @@ -152,7 +152,7 @@ public void receiveCSPReport(@RequestBody String reportData) { // } } - @ApiOperation(value = "安全代码:特殊字符实体转义", notes = "特殊字符实体转义是一种将 HTML 中的特殊字符转换为预定义实体表示的过程。这种转义是为了确保在 HTML 页面中正确显示特定字符,同时避免它们被浏览器误解为 HTML 标签或JavaScript代码的一部分,从而导致页面结构混乱或安全漏洞。") + @ApiOperation(value = "安全代码:HTML正文输出编码", notes = "将HTML正文文本中的特殊字符编码为实体,避免浏览器把不可信数据解析为HTML标签或JavaScript。不同输出上下文需要使用不同编码策略") @GetMapping("/safe3") @ResponseBody @ApiImplicitParams({ @@ -170,11 +170,11 @@ public R safe3(@ApiParam(name = "type", value = "类型", required = true) @Requ payload = StringUtils.replace(payload, "'", "'"); payload = StringUtils.replace(payload, "/", "/"); filterContented = payload; - log.info("[-]XSS-反射性-内容安全策略-CSP防护-原生实体转移:" + payload); + log.info("[-]XSS-反射型-HTML正文输出编码-手动编码:" + payload); break; case "spring": filterContented = HtmlUtils.htmlEscape(payload); - log.info("[-]XSS-反射性-内容安全策略-CSP防护-Spring框架:" + payload); + log.info("[-]XSS-反射型-HTML正文输出编码-Spring框架:" + payload); break; default: return R.error("参数输入有误!"); @@ -182,7 +182,7 @@ public R safe3(@ApiParam(name = "type", value = "类型", required = true) @Requ return R.ok(filterContented); } - @ApiOperation(value = "安全代码:HttpOnly配置", notes = "HttpOnly是HTTP响应头属性,用于增强Web应用程序安全性。它防止客户端脚本访问(只能通过http/https协议访问)带有HttpOnly标记的 cookie,从而减少跨站点脚本攻击(XSS)的风险。") + @ApiOperation(value = "安全代码:HttpOnly配置", notes = "HttpOnly可以阻止客户端脚本直接读取带有该属性的Cookie,降低XSS窃取Cookie的影响,但不能修复XSS本身") @RequestMapping(value = "/safe4", method = RequestMethod.GET) @ResponseBody @ApiImplicitParam(name = "payload", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class) diff --git a/src/main/java/top/whgojp/modules/xss/controller/StoreController.java b/src/main/java/top/whgojp/modules/xss/controller/StoreController.java index a4ecf20..1ddb25d 100644 --- a/src/main/java/top/whgojp/modules/xss/controller/StoreController.java +++ b/src/main/java/top/whgojp/modules/xss/controller/StoreController.java @@ -45,7 +45,7 @@ public String xssStore() { return "vul/xss/store"; } - @ApiOperation(value = "漏洞场景:原生无过滤", notes = "原生漏洞场景,未加任何过滤,将用户输入存储到数据库中") + @ApiOperation(value = "漏洞场景:原生无过滤", notes = "原生漏洞场景,未加任何过滤,将用户输入和User-Agent持久化;后续页面不安全渲染时触发存储型XSS") @PostMapping("/vul") @ResponseBody @ApiImplicitParam(name = "payload", value = "请求参数", dataType = "String", paramType = "query", dataTypeClass = String.class) diff --git a/src/main/resources/static/js/staticcode.js b/src/main/resources/static/js/staticcode.js index ee96d45..a5200a6 100755 --- a/src/main/resources/static/js/staticcode.js +++ b/src/main/resources/static/js/staticcode.js @@ -4,7 +4,7 @@ * @email: whgojp@foxmail.com * @Date: 2024/5/19 19:03 */ -const vul1ReflectRaw = "// 原生漏洞场景,未加任何过滤,Controller接口返回Json类型结果\n" + +const vul1ReflectRaw = "// 原生漏洞场景,未加任何过滤,Controller接口返回JSON类型结果\n" + "public R vul1(String payload) {\n" + " return R.ok(payload);\n" + "}\n" + @@ -14,7 +14,7 @@ const vul1ReflectRaw = "// 原生漏洞场景,未加任何过滤,Controller接 "// \"msg\": \"\",\n" + "// \"code\": 0\n" + "// }\n" + - "// payload在json中是不会触发xss的 需要解析到页面中\n" + + "// JSON响应本身通常不会直接执行脚本;前端若把字段用innerHTML等方式写入页面,才会触发XSS\n" + "\n" + "// 原生漏洞场景,未加任何过滤,Controller接口返回String类型结果\n" + "public String vul2(String payload) {\n" + @@ -35,7 +35,7 @@ const vul2ReflectContentType = "// Tomcat内置HttpServletResponse,Content-Typ " ...\n" + " }\n" + "}" -const safe1CheckUserInput = "// 对用户输入的数据进行验证和过滤,确保不包含恶意代码。使用白名单过滤,只允许特定类型的输入,如纯文本或指定格式的数据\n" + +const safe1CheckUserInput = "// 使用白名单限制输入格式,适合约束字段类型;最终仍需根据输出位置进行上下文编码\n" + "// 前端校验代码\n" + "var whitelistRegex = /^[a-zA-Z0-9_\\s]+$/;\n" + "\n" + @@ -55,19 +55,20 @@ const safe1CheckUserInput = "// 对用户输入的数据进行验证和过滤, "if (matcher.matches()){\n" + " return R.ok(payload);\n" + "}else return R.error(\"输入内容包含非法字符,请检查输入\");" -const safe2CSP = "// 内容安全策略(Content Security Policy)是一种由浏览器实施的安全机制,旨在减少和防范跨站脚本攻击(XSS)等安全威胁。它通过允许网站管理员定义哪些内容来源是可信任的,从而防止恶意内容的加载和执行\n" + +const safe2CSP = "// 内容安全策略(Content Security Policy)是浏览器实施的额外防护层,可降低恶意脚本加载和执行风险,但不能替代输出编码与安全DOM/模板用法\n" + "// 前端Meta配置\n" + "\n" + "\n" + "\n" + "// 后端Header配置\n" + "public String safe2(String payload,HttpServletResponse response) {\n" + - " response.setHeader(\"Content-Security-Policy\",\"default-src self\");\n" + + " response.setHeader(\"Content-Security-Policy\", \"default-src 'self'; script-src 'self'\");\n" + + " response.setHeader(\"Content-Security-Policy-Report-Only\", \"default-src 'self'; report-uri /xss/reflect/csp-report-endpoint\");\n" + " return payload;\n" + "}" -const safe3EntityEscape = '// 特殊字符实体转义是一种将HTML中的特殊字符转换为预定义实体表示的过程\n' + - '// 这种转义是为了确保在HTML页面中正确显示特定字符,同时避免它们被浏览器误解为HTML标签或JavaScript代码的一部分,从而导致页面结构混乱或安全漏洞\n' + +const safe3EntityEscape = '// HTML正文输出编码会将特殊字符转换为HTML实体,避免浏览器把不可信数据解析为标签或脚本\n' + + '// 注意:HTML属性、URL、JavaScript字符串、CSS等不同上下文需要使用不同的编码或白名单校验策略\n' + 'public R safe3(@ApiParam(String type, String payload) {\n' + ' String filterContented = "";\n' + ' switch (type){\n' + @@ -87,7 +88,7 @@ const safe3EntityEscape = '// 特殊字符实体转义是一种将HTML中的特 ' }\n' + '}' -const safe4HttpOnly = "// HttpOnly是HTTP响应头属性,用于增强Web应用程序安全性。它防止客户端脚本访问(只能通过http/https协议访问)带有HttpOnly标记的 cookie,从而减少跨站点脚本攻击(XSS)的风险\n" + +const safe4HttpOnly = "// HttpOnly可以阻止客户端脚本直接读取带有该属性的Cookie,降低XSS窃取Cookie的影响,但不能修复XSS本身\n" + "// 单个接口配置\n" + "public R safe4(String payload, HttpServletRequest request,HttpServletResponse response) {\n" + " Cookie cookie = request.getCookies()[ueditor];\n" + @@ -118,7 +119,7 @@ const safe4HttpOnly = "// HttpOnly是HTTP响应头属性,用于增强Web应用 " ...\n" + "}" -const vul1StoreRaw = "// 原生漏洞场景,未加任何过滤,将用户输入存储到数据库中\n" + +const vul1StoreRaw = "// 原生漏洞场景,未加任何过滤,将用户输入和User-Agent持久化;后续页面不安全渲染时触发存储型XSS\n" + "// Controller层\n" + "public R vul(String payload,HttpServletRequest request) {\n" + " String ua = request.getHeader(\"User-Agent\");\n" + @@ -139,7 +140,7 @@ const vul1StoreRaw = "// 原生漏洞场景,未加任何过滤,将用户输入 " values (#{content,jdbcType=VARCHAR},#{ua,jdbcType=VARCHAR}, #{date,jdbcType=VARCHAR})\n" + "" -const safe1StoreEntityEscape = "// 表格数据渲染\n" + +const safe1StoreEntityEscape = "// 表格数据渲染:数据库仍保存原始值,输出到HTML页面前按HTML正文文本编码\n" + "table.render({\n" + "\t...\n" + " cols: [\n" + @@ -151,10 +152,13 @@ const safe1StoreEntityEscape = "// 表格数据渲染\n" + " return escapeHtml(d.ua); \n" + " }},\n" + " \t...\n" + - "// 方法一、HTML 实体转义函数\n" + + "// 方法一、HTML正文输出编码函数\n" + "function escapeHtml(html) {\n" + + " if (html === null || html === undefined) {\n" + + " return '';\n" + + " }\n" + " var text = document.createElement(\"textarea\");\n" + - " text.textContent = html;\n" + + " text.textContent = String(html);\n" + " return text.innerHTML;\n" + "}\n" + "// 方法二、JavaScript的文本节点\n" + @@ -210,11 +214,38 @@ const vul1DomRaw = "// 1. innerHTML XSS\n" + "});\n" + "form.on('submit(document-domain)', function(data) {\n" + " var payload = data.field.documentPayload;\n" + - " document.domain = payload; // 漏洞点:直接修改document.domain\n" + + " document.domain = payload; // 风险点:直接修改document.domain会放宽同源边界或造成异常行为\n" + " return false;\n" + "});" -const vul1OtherUpload = "public String uploadFile(MultipartFile file, String suffix,String path) throws IOException {\n" + +const safeDomCode = "// 1. 普通文本输出:使用textContent,不解析HTML\n" + + "document.getElementById('safe-dom-result').textContent = userInput;\n" + + "\n" + + "// 2. URL跳转:校验协议白名单,拒绝javascript:、data:等危险协议\n" + + "var url = new URL(userInput, window.location.origin);\n" + + "var allowedProtocols = ['http:', 'https:'];\n" + + "if (allowedProtocols.indexOf(url.protocol) === -1) {\n" + + " throw new Error('dangerous protocol');\n" + + "}\n" + + "\n" + + "// 3. 替代eval:使用命令白名单映射,而不是执行用户输入\n" + + "var actions = {\n" + + " showTime: function () { return new Date().toLocaleString(); },\n" + + " showLocation: function () { return window.location.pathname; }\n" + + "};\n" + + "var action = actions[userInput];\n" + + "if (action) {\n" + + " action();\n" + + "}\n" + + "\n" + + "// 4. DOM API:创建文本节点,不拼接HTML字符串\n" + + "var node = document.createTextNode(userInput);\n" + + "element.appendChild(node);\n" + + "\n" + + "// 如果业务必须展示富文本,应先使用白名单HTML净化库处理后再渲染\n" + +const vul1OtherUpload = "// 上传可被浏览器或预览服务解析的HTML/SVG/XML/PDF等文件,后续访问文件时可能触发XSS或内容安全问题\n" + + "public String uploadFile(MultipartFile file, String suffix,String path) throws IOException {\n" + " String uploadFolderPath = sysConstant.getUploadFolder();\n" + " try {\n" + " String fileName = +DateUtil.current() + \".\"+suffix;\n" + @@ -230,7 +261,8 @@ const vul1OtherUpload = "public String uploadFile(MultipartFile file, String suf " }\n" + "}" -const vul2OtherTemplate = "public String handleTemplateInjection(String payload,String type, Model model) {\n" + +const vul2OtherTemplate = "// th:utext会把内容作为HTML渲染;th:text会进行HTML转义\n" + + "public String handleTemplateInjection(String payload,String type, Model model) {\n" + " if (\"html\".equals(type)) {\n" + " model.addAttribute(\"html\", payload);\n" + " } else if (\"text\".equals(type)) {\n" + diff --git a/src/main/resources/templates/vul/xss/dom-href.html b/src/main/resources/templates/vul/xss/dom-href.html index 9b0025a..09db862 100644 --- a/src/main/resources/templates/vul/xss/dom-href.html +++ b/src/main/resources/templates/vul/xss/dom-href.html @@ -14,9 +14,9 @@
- hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分(从 # 号开始的部分)。当其作为可控参数传入eval()时则会存在DOM型XSS漏洞。
+ hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分(从 # 号开始的部分)。当其作为可控参数进入 location.href、innerHTML、eval() 等危险 Sink 时,可能产生DOM型XSS漏洞。
通过 location.hash 的方式,将参数写在 # 号后,既能让JS读取到该参数,
- 又不让该参数传入到服务器,从而避免了WAF的检测。
+ 又不让该参数传入到服务器,因此服务端日志或部分网关/WAF可能看不到这段载荷。
变量hash作为可控部分,并带入url中,变量hash控制的是#之后的部分,
@@ -38,4 +38,4 @@