diff --git a/CHANGELOG.md b/CHANGELOG.md index e6a410c..0cba62f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## v2.2.0 +### feat: Fuse Servlet and Spring rules for enhanced integration +- feat: Servlet and Spring rules fusion ## v2.1.3 ### fix: Update insecure cookie and command injection rules - fix: Require insecure cookie to flow in response and add tests diff --git a/rules/java/lib/generic/seam-untrusted-data-source.yaml b/rules/java/lib/generic/seam-untrusted-data-source.yaml index 7de3a3c..7ff97e3 100644 --- a/rules/java/lib/generic/seam-untrusted-data-source.yaml +++ b/rules/java/lib/generic/seam-untrusted-data-source.yaml @@ -4,6 +4,44 @@ rules: lib: true message: Untrusted data flows from here severity: NOTE + metadata: + short-description: Untrusted data source in Seam/JSF request context + full-description: |- + This is a source rule for untrusted input in Seam/JSF applications. + It marks values as tainted when they are obtained from request parameter/header/cookie maps + or injected into Seam components from request context. + + Typical source locations matched by this rule include: + - `@In`-annotated Seam setter parameters + - `FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get(...)` + - `...getRequestHeaderMap().get(...)` + - `...getRequestCookieMap().get(...)` + + Example source usage (tainted): + + ```java + import javax.faces.context.FacesContext; + + public class LoginAction { + public void login() { + String username = FacesContext.getCurrentInstance() + .getExternalContext() + .getRequestParameterMap() + .get("username"); // source: untrusted + + process(username); + } + + private void process(String input) { + // A vulnerability is reported only when tainted input reaches a vulnerable API pattern. + } + } + ``` + + Recommended handling at the boundary: + - Treat all JSF request map values as untrusted. + - Validate and normalize input before passing to logging, query, template, or command-execution APIs. + - Keep security checks and canonicalization centralized for consistent handling. languages: - java pattern-either: diff --git a/rules/java/lib/generic/servlet-untrusted-data-source.yaml b/rules/java/lib/generic/servlet-untrusted-data-source.yaml index 775a449..b5012df 100644 --- a/rules/java/lib/generic/servlet-untrusted-data-source.yaml +++ b/rules/java/lib/generic/servlet-untrusted-data-source.yaml @@ -4,6 +4,43 @@ rules: lib: true severity: NOTE message: Untrusted data flows from here + metadata: + short-description: Untrusted data source in Servlet/JSP entrypoints + full-description: |- + This is a source rule for untrusted input in Servlet and JSP code paths. + It marks values as tainted when they originate from HTTP request-controlled entrypoints, + request body readers, or uploaded-file metadata APIs. + + Typical source locations matched by this rule include: + - `HttpServletRequest` parameters in servlet entry methods (`doGet`, `doPost`, etc.) + - Values returned by `MessageBodyReader.readFrom(...)` + - File names from multipart upload parsing (`parseRequest(...).getName()`) + - File names from `Part.getSubmittedFileName()` + + Example source usage (tainted): + + ```java + import jakarta.servlet.http.HttpServlet; + import jakarta.servlet.http.HttpServletRequest; + import jakarta.servlet.http.HttpServletResponse; + + public class DownloadServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) { + String filename = request.getParameter("file"); // source: untrusted + handleFile(filename); + } + + private void handleFile(String input) { + // Later flow to a vulnerable API pattern is what creates a finding. + } + } + ``` + + Recommended handling at the boundary: + - Treat all request-derived values as untrusted by default. + - Validate expected shape (type, length, charset, allowlist) as early as possible. + - Convert to strongly typed/domain values before business logic and security-sensitive calls. languages: - java patterns: diff --git a/rules/java/lib/spring/untrusted-data-source.yaml b/rules/java/lib/spring/untrusted-data-source.yaml index 66ac51c..2047bc4 100644 --- a/rules/java/lib/spring/untrusted-data-source.yaml +++ b/rules/java/lib/spring/untrusted-data-source.yaml @@ -4,6 +4,44 @@ rules: lib: true severity: NOTE message: Untrusted data flows from here + metadata: + short-description: Untrusted data source in Spring request handlers + full-description: |- + This is a source rule for untrusted input in Spring MVC / Spring Boot handlers. + It marks method parameters and request-derived values as tainted when they can be + controlled by an external caller. + + Typical source locations matched by this rule include: + - Handler method parameters in `@RequestMapping`, `@GetMapping`, `@PostMapping`, etc. + - Values returned by `MessageBodyReader.readFrom(...)` + - Cookie values resolved via `WebUtils.getCookie(...).getValue()` + + Example source usage (tainted): + + ```java + import org.springframework.web.bind.annotation.GetMapping; + import org.springframework.web.bind.annotation.RequestParam; + import org.springframework.web.bind.annotation.RestController; + + @RestController + class SearchController { + @GetMapping("/search") + String search(@RequestParam String q) { + // q is a source: untrusted user-controlled input + return doSearch(q); + } + + private String doSearch(String input) { + // Vulnerability findings appear only when this value reaches an unsafe usage. + return input; + } + } + ``` + + Recommended handling at the boundary: + - Assume all request parameters, headers, cookies, and bodies are untrusted. + - Apply validation constraints (Bean Validation / explicit checks) close to controller boundaries. + - Normalize and map external input to typed internal models before unsafe usage. languages: - java patterns: diff --git a/rules/java/lib/spring/untrusted-path-source.yaml b/rules/java/lib/spring/untrusted-path-source.yaml index e0bc250..f3d1a60 100644 --- a/rules/java/lib/spring/untrusted-path-source.yaml +++ b/rules/java/lib/spring/untrusted-path-source.yaml @@ -4,6 +4,45 @@ rules: lib: true severity: NOTE message: Untrusted data flows from here + metadata: + short-description: Untrusted path-like source in Spring handlers + full-description: |- + This is a source rule for path-like untrusted data in Spring handlers. + It marks values as tainted when they come from request-controlled mapping parameters, + wildcard route captures, body readers, or cookies that can later influence file/path operations. + + Typical source locations matched by this rule include: + - `@PathVariable` values and wildcard mapping captures in `@RequestMapping` methods + - Other handler parameters in mapped methods that can carry path segments + - Values from `MessageBodyReader.readFrom(...)` + - Cookie-derived values via `WebUtils.getCookie(...).getValue()` + + Example source usage (tainted): + + ```java + import org.springframework.web.bind.annotation.GetMapping; + import org.springframework.web.bind.annotation.PathVariable; + import org.springframework.web.bind.annotation.RestController; + + @RestController + class FileController { + @GetMapping("/files/{name}") + byte[] readFile(@PathVariable String name) { + // name is untrusted path-related input (source) + return load(name); + } + + private byte[] load(String candidatePath) { + // A finding is raised when this reaches a path traversal operation. + return new byte[0]; + } + } + ``` + + Recommended handling at the boundary: + - Prefer opaque file IDs over raw path input from clients. + - Validate allowed filename patterns and reject traversal tokens (`..`, absolute paths, encoded variants). + - Normalize and enforce base-directory checks before any filesystem operation. languages: - java patterns: diff --git a/rules/java/security/code-injection.yaml b/rules/java/security/code-injection.yaml index 0e76c4d..421b8f2 100644 --- a/rules/java/security/code-injection.yaml +++ b/rules/java/security/code-injection.yaml @@ -1,5 +1,5 @@ rules: - - id: groovy-injection-in-servlet-app + - id: groovy-injection severity: ERROR message: >- Potential code injection: Groovy invocation with user-controlled input. @@ -8,68 +8,41 @@ rules: - CWE-94 short-description: Found Groovy invocation with user-controlled input full-description: |- - Groovy injection is a code-injection vulnerability that arises when untrusted input - is used to build and execute Groovy code at runtime - (for example via GroovyShell.evaluate, Eval.me, or a Groovy ScriptEngine). - If an attacker can control any part of the Groovy script string, they may execute - arbitrary Groovy/Java code with the application's privileges, leading to full server compromise, - data exfiltration, or lateral movement. - - Example (vulnerable): - ``` - String script = request.getParameter("script"); // user-controlled - GroovyShell shell = new GroovyShell(); - Object result = shell.evaluate(script); // RCE: attacker controls code - ``` + Groovy code injection occurs when untrusted input reaches Groovy evaluation APIs + such as `GroovyShell.evaluate(...)`, `parse(...)`, or `Eval.me(...)`. - To remediate this issue, avoid evaluating dynamically constructed Groovy code whenever possible; - prefer static scripts, fixed command mappings, or other non-code-based configuration. - If dynamic evaluation is truly required, strictly validate and constrain input - (for example, by whitelisting a very limited grammar), avoid concatenating raw user data into scripts, - and use Groovy sandboxing mechanisms such as SecureASTCustomizer plus a hardened SecurityManager. - references: - - https://owasp.org/www-community/attacks/Code_Injection - provenance: https://find-sec-bugs.github.io/bugs.htm#GROOVY_SHELL - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/code-injection-sinks.yaml#dangerous-groovy-shell - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + Vulnerable example: - - id: groovy-injection-in-spring-app - severity: ERROR - message: >- - Potential code injection: Groovy invocation with user-controlled input. This is a potential code injection. - metadata: - cwe: - - CWE-94 - short-description: Found Groovy invocation with user-controlled input - full-description: |- - Groovy injection is a code-injection vulnerability that arises when untrusted input - is used to build and execute Groovy code at runtime - (for example via GroovyShell.evaluate, Eval.me, or a Groovy ScriptEngine). - If an attacker can control any part of the Groovy script string, they may execute - arbitrary Groovy/Java code with the application's privileges, leading to full server compromise, - data exfiltration, or lateral movement. - - Example (vulnerable): + ```java + import groovy.lang.GroovyShell; + + public class ScriptService { + public Object run(String scriptFromRequest) { + GroovyShell shell = new GroovyShell(); + return shell.evaluate(scriptFromRequest); // VULNERABLE + } + } ``` - String script = request.getParameter("script"); // user-controlled - GroovyShell shell = new GroovyShell(); - Object result = shell.evaluate(script); // RCE: attacker controls code + + Safe example: + + ```java + import groovy.lang.Binding; + import groovy.lang.GroovyShell; + + public class ScriptService { + public Object run(int a, int b) { + String trustedScript = "x + y"; + Binding binding = new Binding(); + binding.setVariable("x", a); + binding.setVariable("y", b); + GroovyShell shell = new GroovyShell(binding); + return shell.evaluate(trustedScript); + } + } ``` - To remediate this issue, avoid evaluating dynamically constructed Groovy code whenever possible; - prefer static scripts, fixed command mappings, or other non—code-based configuration. - If dynamic evaluation is truly required, strictly validate and constrain input - (for example, by whitelisting a very limited grammar), avoid concatenating raw user data into scripts, - and use Groovy sandboxing mechanisms such as SecureASTCustomizer plus a hardened SecurityManager. + Key vulnerable patterns covered by this rule include Groovy shell parse/evaluate APIs and `groovy.util.Eval.*`. references: - https://owasp.org/www-community/attacks/Code_Injection provenance: https://find-sec-bugs.github.io/bugs.htm#GROOVY_SHELL @@ -78,14 +51,17 @@ rules: mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/code-injection-sinks.yaml#dangerous-groovy-shell as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - id: ognl-injection-in-servlet-app + - id: ognl-injection severity: ERROR message: >- Potential code injection: OGNL expression construction from user-controlled input. @@ -94,81 +70,41 @@ rules: - CWE-94 short-description: OGNL expression construction from user-controlled input full-description: |- - OGNL (Object-Graph Navigation Language) injection is a type of expression-language injection - vulnerability where untrusted user input is evaluated as an OGNL expression on the server. - OGNL is powerful and can access object properties, call methods, and instantiate classes; - when attackers can influence OGNL expressions, they may achieve remote code execution (RCE), - read or modify sensitive data, or bypass access controls. This vulnerability has historically - affected frameworks such as Apache Struts 2 that use OGNL for data binding and expression - evaluation in views and configuration. + OGNL injection occurs when untrusted input reaches OGNL evaluation APIs + (`Ognl.getValue(...)`, Struts OGNL utility/value-stack evaluation methods). - Vulnerable code sample: + Vulnerable example: ```java import ognl.Ognl; import ognl.OgnlContext; - public class OgnlController { - - public Object evaluateExpression(javax.servlet.http.HttpServletRequest request) throws Exception { - // Attacker controls the "expr" parameter - String expr = request.getParameter("expr"); - - // Build OGNL context from application objects - OgnlContext context = new OgnlContext(); - context.put("userService", new UserService()); - context.put("system", java.lang.System.class); - - // VULNERABLE: evaluating untrusted input as an OGNL expression - Object value = Ognl.getValue(expr, context, context.getRoot()); - - return value; // result potentially exposed to the client + public class OgnlService { + public Object eval(String exprFromRequest) throws Exception { + OgnlContext ctx = new OgnlContext(); + return Ognl.getValue(exprFromRequest, ctx, ctx.getRoot()); // VULNERABLE } } ``` - In a Struts 2 application, a similar problem can occur when user-controllable data ends up - in OGNL-evaluated attributes (e.g., misconfigured tags, parameters bound directly to OGNL - expressions in XML or JSPs). - - To remediate this issue, never evaluate OGNL (or any expression language) on untrusted input, - and ensure the framework is configured and updated to prevent such evaluation paths. - - Key steps: - - Upgrade to the latest secure version of frameworks using OGNL (e.g., latest Struts 2 with - all OGNL-related security patches). - - Disable or restrict dynamic method invocation and other features that expand the OGNL attack surface. - - Avoid building OGNL expressions from request parameters or any user-controlled data. - - Use strict input binding/whitelisting for parameters and models rather than generic expression evaluation. - - Enforce strong input validation and encoding, but do not rely on it alone to secure expression evaluation. - - Safer approach (do not evaluate arbitrary OGNL; map user input to allowed operations): + Safe example: ```java - public class SafeController { - - private final UserService userService = new UserService(); - - public Object handleRequest(javax.servlet.http.HttpServletRequest request) { - String action = request.getParameter("action"); - - // Whitelist of allowed actions; no OGNL evaluation - if ("getProfile".equals(action)) { - String userId = request.getParameter("userId"); - return userService.getProfile(userId); - } else if ("listUsers".equals(action)) { + public class OgnlService { + public Object dispatch(String actionFromRequest, UserService userService) { + if ("profile".equals(actionFromRequest)) { + return userService.currentProfile(); + } + if ("list".equals(actionFromRequest)) { return userService.listUsers(); - } else { - throw new IllegalArgumentException("Unsupported action"); } + throw new IllegalArgumentException("Unsupported action"); } } ``` - In Struts 2, prefer: - - Standard action properties and setters (no dynamic expressions built from user input). - - Secure configuration (e.g., `struts2.allowed.action.names`, disabling `dynamicMethodInvocation`, - using “strict method invocation” and secure tag usage). + Key vulnerable patterns covered by this rule include OGNL direct evaluators and Struts OGNL helpers + when tainted expression text is evaluated. references: - https://struts.apache.org/security/#do-not-use-incoming-untrusted-user-input-in-forced-expression-evaluation - https://owasp.org/www-community/attacks/Expression_Language_Injection @@ -179,244 +115,69 @@ rules: join: refs: - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/code-injection-sinks.yaml#ognl-injection-sinks - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$INPUT' - - - id: ognl-injection-in-spring-app - severity: ERROR - message: >- - Potential code injeciton: OGNL expression construction from user-controlled input. - metadata: - cwe: - - CWE-94 - short-description: OGNL expression construction from user-controlled input - full-description: |- - The Object Graph Navigation Language (OGNL) is an expression language that allows access to - Java objects and properties stored in an ActionContext. Usage of these low-level - functions is discouraged because they can effectively execute strings as code, leading to - remote code execution vulnerabilities. Consider using struts tags when processing - user-supplied input and templates. - - Much like the Struts security guide recommending to not use raw `${}` EL expressions, - do not call or use the following OGNL packages with user-supplied input: - - - `com.opensymphony.xwork2.ognl` - - `com.opensymphony.xwork2.util` - - `com.opensymphony.xwork2.util.reflection` - - `org.apache.struts2.util.StrutsUtil` - references: https://struts.apache.org/security/#do-not-use-incoming-untrusted-user-input-in-forced-expression-evaluation - provenance: https://find-sec-bugs.github.io/bugs.htm#OGNL_INJECTION - languages: - - java - mode: join - join: - refs: + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/code-injection-sinks.yaml#ognl-injection-sinks as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$INPUT' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$INPUT' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$INPUT' - - - id: script-engine-injection-in-servlet-app + - id: script-engine-injection severity: ERROR message: | - Potential code injection: ScriptEngine executes user-controled code. + Potential code injection: ScriptEngine executes user-controlled code. metadata: cwe: - CWE-94 short-description: Injection into javax.script.ScriptEngine full-description: |- - Injection into `javax.script.ScriptEngine` occurs when untrusted input is evaluated as code by a - Java Scripting Engine (e.g., Nashorn, JavaScript, etc.). - If attacker-controlled data is passed directly to `ScriptEngine.eval()` (or similar methods), - an attacker can execute arbitrary script code in the context of the application's JVM. - This can lead to data exfiltration, privilege escalation, or full remote code execution, - depending on what the script engine can access (e.g., Java classes, filesystem, network, environment variables). + Script-engine injection occurs when untrusted input reaches Java script-evaluation APIs + such as `ScriptEngine.eval(...)` and `Invocable.invoke*` with tainted script content. + + Vulnerable example: ```java - // Vulnerable code sample import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; - import javax.script.ScriptException; - import javax.servlet.http.HttpServletRequest; - - public class ScriptEvaluator { - - public Object evaluateExpression(HttpServletRequest request) throws ScriptException { - // Attacker controls this parameter (e.g., ?expr=...) - String expr = request.getParameter("expr"); - - ScriptEngineManager manager = new ScriptEngineManager(); - ScriptEngine engine = manager.getEngineByName("javascript"); // or "nashorn", "groovy", etc. - - // VULNERABLE: directly evaluates attacker-controlled code - return engine.eval(expr); - } - } - ``` - - To remediate this issue, do not execute untrusted input as script code. Treat user input strictly - as data, not as instructions. Where scripting is necessary: - - 1. **Avoid passing user input into `eval`-like APIs as code.** - - If you only need to support simple operations (e.g., arithmetic), implement or use a dedicated - expression parser that does not allow arbitrary function calls or access to the underlying JVM. - - Never let the user control full script bodies, function definitions, or arbitrary expressions - that the engine will execute. - - 2. **Use static scripts and bindings for data.** - Define the script code in your application (trusted), and pass user input only as variables/bindings: - - ```java - import javax.script.*; - - public class SafeScriptEvaluator { - - private final ScriptEngine engine; - private final CompiledScript compiled; // optional, but more efficient and safer pattern - public SafeScriptEvaluator() throws ScriptException { - ScriptEngineManager manager = new ScriptEngineManager(); - this.engine = manager.getEngineByName("javascript"); - - // Trusted, static script embedded in the application - String script = "function calculate(a, b) { return a + b; } " + - "calculate(a, b);"; - - this.compiled = ((Compilable) engine).compile(script); - } - - public Object safeEvaluate(int a, int b) throws ScriptException { - Bindings bindings = engine.createBindings(); - bindings.put("a", a); // user input as data - bindings.put("b", b); // user input as data - - // Only the trusted script runs; user data cannot change the code - return compiled.eval(bindings); + public class ScriptEngineService { + public Object eval(String exprFromRequest) throws Exception { + ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript"); + return engine.eval(exprFromRequest); // VULNERABLE } } ``` - 3. **Restrict the script engine's capabilities (sandboxing), if supported.** - - Disable or strictly limit access to `Java.type`, reflection, file I/O, process execution, - and network APIs from within the script. - - Run script engines in a constrained environment (e.g., separate process/container, or with - appropriate security manager / policy, if available). - - 4. **Validate and constrain inputs on a strict allowlist.** - - If you must accept some form of “expression” from users, define a grammar or a very narrow - allowed syntax and reject anything outside that set. - - Do not assume that “it's just math” is safe; enforce it programmatically. - references: - - https://owasp.org/www-community/attacks/Code_Injection - provenance: https://find-sec-bugs.github.io/bugs.htm#SCRIPT_ENGINE_INJECTION - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/code-injection-sinks.yaml#dangerous-script-engine-eval - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: script-engine-injection-in-spring-app - severity: ERROR - message: >- - Potential code injection: ScriptEngine executes user-controled code. - metadata: - cwe: - - CWE-94 - short-description: Injection into javax.script.ScriptEngine - full-description: |- - Injection into `javax.script.ScriptEngine` occurs when untrusted input is evaluated as code by a - Java Scripting Engine (e.g., Nashorn, JavaScript, etc.). - If attacker-controlled data is passed directly to `ScriptEngine.eval()` (or similar methods), - an attacker can execute arbitrary script code in the context of the application's JVM. - This can lead to data exfiltration, privilege escalation, or full remote code execution, - depending on what the script engine can access (e.g., Java classes, filesystem, network, environment variables). + Safe example: ```java - // Vulnerable code sample + import javax.script.Bindings; + import javax.script.Compilable; + import javax.script.CompiledScript; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; - import javax.script.ScriptException; - import javax.servlet.http.HttpServletRequest; - - public class ScriptEvaluator { - public Object evaluateExpression(HttpServletRequest request) throws ScriptException { - // Attacker controls this parameter (e.g., ?expr=...) - String expr = request.getParameter("expr"); + public class ScriptEngineService { + private final ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript"); + private final CompiledScript compiled; - ScriptEngineManager manager = new ScriptEngineManager(); - ScriptEngine engine = manager.getEngineByName("javascript"); // or "nashorn", "groovy", etc. - - // VULNERABLE: directly evaluates attacker-controlled code - return engine.eval(expr); + public ScriptEngineService() throws Exception { + this.compiled = ((Compilable) engine).compile("a + b"); } - } - ``` - - To remediate this issue, do not execute untrusted input as script code. Treat user input strictly - as data, not as instructions. Where scripting is necessary: - - 1. **Avoid passing user input into `eval`-like APIs as code.** - - If you only need to support simple operations (e.g., arithmetic), implement or use a dedicated - expression parser that does not allow arbitrary function calls or access to the underlying JVM. - - Never let the user control full script bodies, function definitions, or arbitrary expressions - that the engine will execute. - - 2. **Use static scripts and bindings for data.** - Define the script code in your application (trusted), and pass user input only as variables/bindings: - - ```java - import javax.script.*; - - public class SafeScriptEvaluator { - - private final ScriptEngine engine; - private final CompiledScript compiled; // optional, but more efficient and safer pattern - public SafeScriptEvaluator() throws ScriptException { - ScriptEngineManager manager = new ScriptEngineManager(); - this.engine = manager.getEngineByName("javascript"); - - // Trusted, static script embedded in the application - String script = "function calculate(a, b) { return a + b; } " + - "calculate(a, b);"; - - this.compiled = ((Compilable) engine).compile(script); - } - - public Object safeEvaluate(int a, int b) throws ScriptException { + public Object eval(int a, int b) throws Exception { Bindings bindings = engine.createBindings(); - bindings.put("a", a); // user input as data - bindings.put("b", b); // user input as data - - // Only the trusted script runs; user data cannot change the code + bindings.put("a", a); + bindings.put("b", b); return compiled.eval(bindings); } } ``` - 3. **Restrict the script engine's capabilities (sandboxing), if supported.** - - Disable or strictly limit access to `Java.type`, reflection, file I/O, process execution, - and network APIs from within the script. - - Run script engines in a constrained environment (e.g., separate process/container, or with - appropriate security manager / policy, if available). - - 4. **Validate and constrain inputs on a strict allowlist.** - - If you must accept some form of “expression” from users, define a grammar or a very narrow - allowed syntax and reject anything outside that set. - - Do not assume that “it's just math” is safe; enforce it programmatically. + Key vulnerable patterns covered by this rule include `ScriptEngine.eval` and `Invocable.invokeFunction/invokeMethod` + when script text or callable selector is tainted. references: - https://owasp.org/www-community/attacks/Code_Injection provenance: https://find-sec-bugs.github.io/bugs.htm#SCRIPT_ENGINE_INJECTION @@ -425,14 +186,17 @@ rules: mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/code-injection-sinks.yaml#dangerous-script-engine-eval as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - id: ssti-in-servlet-app + - id: ssti severity: ERROR message: >- Potential template injection: unvalidated user data flows into template engine @@ -442,109 +206,49 @@ rules: - CWE-1336 short-description: Unvalidated user data flows into template engine full-description: |- - Template engine injection (often called server-side template injection, SSTI) is a - vulnerability where untrusted input is interpreted as template code by a server-side template engine - (e.g., FreeMarker, Velocity, Thymeleaf, JSP EL). In Java servlet applications, - this happens when user-controlled data is used to build or select templates, - or is otherwise passed to the template engine in a way that allows it to be executed - rather than treated as plain data. Successful exploitation can lead to information disclosure, - arbitrary server-side code execution, or full compromise of the application server. + Server-side template injection occurs when untrusted input reaches template-engine APIs + as template content, template name, or expression-bearing payload. - ```java - // Vulnerable code sample (Java servlet + FreeMarker) + Vulnerable example: + ```java import freemarker.template.Configuration; import freemarker.template.Template; - import javax.servlet.http.HttpServlet; - import javax.servlet.http.HttpServletRequest; - import javax.servlet.http.HttpServletResponse; import java.io.StringReader; - import java.util.HashMap; - import java.util.Map; - - public class MessageServlet extends HttpServlet { - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) { - try { - // Attacker controls the entire template content - String templateSource = request.getParameter("messageTemplate"); - - Configuration cfg = (Configuration) getServletContext().getAttribute("freemarkerCfg"); - - // VULNERABLE: compiling a template directly from user input - Template t = new Template("userTemplate", new StringReader(templateSource), cfg); - - Map model = new HashMap<>(); - model.put("username", request.getParameter("username")); - - response.setContentType("text/html;charset=UTF-8"); - t.process(model, response.getWriter()); // User input is interpreted as template code - } catch (Exception e) { - throw new RuntimeException(e); - } + public class TemplateService { + public Template compile(Configuration cfg, String templateFromRequest) throws Exception { + // templateFromRequest is untrusted + return new Template("userTpl", new StringReader(templateFromRequest), cfg); // VULNERABLE } } ``` - To remediate this issue, never allow untrusted input to be treated as template code. - Use only static, server-side—controlled templates and ensure user data is inserted strictly as data (variables), - not as executable expressions or template fragments. Also, restrict dangerous template features - (e.g., arbitrary class loading, execution helpers) where possible. - - A safer approach: + Safe example: ```java - // Safer code sample (whitelisted template + data only) - import freemarker.template.Configuration; import freemarker.template.Template; - import javax.servlet.http.HttpServlet; - import javax.servlet.http.HttpServletRequest; - import javax.servlet.http.HttpServletResponse; - import java.util.HashMap; import java.util.Map; - public class MessageServlet extends HttpServlet { - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) { - try { - Configuration cfg = (Configuration) getServletContext().getAttribute("freemarkerCfg"); - - // Use only server-controlled template names (e.g., enum, config, or hard-coded) - String templateName = "message.ftl"; // static template stored on the server - Template t = cfg.getTemplate(templateName); - - // Treat user input as plain data only - String username = request.getParameter("username"); - String messageText = request.getParameter("messageText"); - - Map model = new HashMap<>(); - model.put("username", username); - model.put("messageText", messageText); - - response.setContentType("text/html;charset=UTF-8"); - t.process(model, response.getWriter()); - - } catch (Exception e) { - throw new RuntimeException(e); - } + public class TemplateService { + public String render(Configuration cfg, String templateKey, Map model) throws Exception { + String selected = switch (templateKey) { + case "welcome" -> "welcome.ftl"; + case "receipt" -> "receipt.ftl"; + default -> throw new IllegalArgumentException("Unsupported template"); + }; + + Template tpl = cfg.getTemplate(selected); + java.io.StringWriter out = new java.io.StringWriter(); + tpl.process(model, out); + return out.toString(); } } ``` - Additional hardening steps: - - - Do not construct templates or template fragments (e.g., `new Template(...)`, `engine.process(userInput, ...)`) - from user input, including from HTTP parameters, headers, cookies, or database fields that users can influence. - - Use strict whitelisting when selecting templates (e.g., map allowed IDs to fixed template filenames). - - Configure the template engine in a “safe” mode where available (e.g., disable or restrict access to arbitrary - classes, reflection, or command execution helpers). - - Validate and, if necessary, HTML-escape user-provided strings before rendering, so they are displayed as - text rather than interpreted as markup or expressions. - - Avoid using powerful template engines as general-purpose expression evaluators for user input; - if evaluation is required, use dedicated, sandboxed expression libraries with strong security guarantees. + Key vulnerable patterns covered by this rule include FreeMarker/Velocity/Thymeleaf/Jinjava/Pebble + template compilation and processing APIs with tainted template identifiers/content. references: - https://portswigger.net/web-security/server-side-template-injection provenance: https://github.com/github/codeql/tree/cdd8aa49e16650a96b8993e8745c7672600fe930/java/ql/lib/ext @@ -554,160 +258,14 @@ rules: join: refs: - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/template-injection-sinks.yaml#java-ssti-sinks - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: ssti-in-spring-app - severity: ERROR - message: | - Potential template injection: unvalidated user data flows into template engine - metadata: - cwe: - - CWE-94 - - CWE-1336 - short-description: Unvalidated user data flows into template engine - full-description: |- - Template engine injection (server-side template injection, SSTI) in Spring MVC/Spring Boot applications - occurs when user-controlled data is interpreted as template or expression code by the view layer or by Spring Expression Language (SpEL). - In typical Spring apps this happens when untrusted input is used as a template body, fragment, or expression, - or when SpEL is evaluated on data that comes from the request. Successful exploitation can lead to arbitrary - server-side code execution, data exfiltration, or full compromise of the application. - - ```java - // Vulnerable Spring MVC + Thymeleaf example (SSTI via StringTemplateResolver) - - import org.springframework.stereotype.Controller; - import org.springframework.web.bind.annotation.*; - import org.thymeleaf.context.Context; - import org.thymeleaf.spring5.SpringTemplateEngine; - - @Controller - @RequestMapping("/messages") - public class MessageController { - - private final SpringTemplateEngine templateEngine; - - public MessageController(SpringTemplateEngine templateEngine) { - this.templateEngine = templateEngine; - } - - @PostMapping("/preview") - @ResponseBody - public String preview(@RequestParam("template") String template, - @RequestParam("username") String username) { - - // Attacker fully controls "template" request parameter. - // With a StringTemplateResolver configured, this is treated as a full Thymeleaf template. - - Context ctx = new Context(); - ctx.setVariable("username", username); - - // VULNERABLE: user input is parsed and executed as a Thymeleaf/SpringEL template - return templateEngine.process(template, ctx); - } - } - - // Example of insecure configuration somewhere in @Configuration: - - import org.springframework.context.annotation.Bean; - import org.springframework.context.annotation.Configuration; - import org.thymeleaf.templateresolver.StringTemplateResolver; - import org.thymeleaf.templatemode.TemplateMode; - - @Configuration - public class ThymeleafConfig { - - @Bean - public SpringTemplateEngine templateEngine() { - SpringTemplateEngine engine = new SpringTemplateEngine(); - engine.addTemplateResolver(stringTemplateResolver()); // processes raw strings as templates - return engine; - } - - @Bean - public StringTemplateResolver stringTemplateResolver() { - StringTemplateResolver resolver = new StringTemplateResolver(); - resolver.setTemplateMode(TemplateMode.HTML); - resolver.setCacheable(false); - return resolver; - } - } - ``` - - In this example, an attacker can send a payload such as: - - ```text - [[${T(java.lang.Runtime).getRuntime().exec('id')}]] - ``` - - which, depending on the exact Spring/Thymeleaf/SpEL version and configuration, - may allow arbitrary command or method execution on the server. - - To remediate this issue, never evaluate or compile templates or expressions directly - from untrusted input in Spring. Use only server-controlled template names with standard `ViewResolver`s, - and treat user data strictly as data passed to the model, not as template instructions. - - Safer Spring MVC + Thymeleaf example: - - ```java - // Safe-ish version: static templates only, user input is data - - import org.springframework.stereotype.Controller; - import org.springframework.ui.Model; - import org.springframework.web.bind.annotation.*; - - @Controller - @RequestMapping("/messages") - public class MessageController { - - @PostMapping("/preview") - public String preview(@RequestParam("username") String username, - @RequestParam("messageText") String messageText, - Model model) { - - // Use a fixed, server-side template name only - String templateName = "messagePreview"; // e.g. src/main/resources/templates/messagePreview.html - - // User input is added as plain data to the model - model.addAttribute("username", username); - model.addAttribute("messageText", messageText); - - // The view resolver will select the template based on this fixed name - return templateName; - } - } - ``` - - Key hardening steps in Spring-based applications: - - - Do **not** use `StringTemplateResolver`, `FreeMarkerTemplateUtils.processTemplateIntoString`, - or similar APIs on untrusted strings. Only process templates that reside on the server and are under your control. - - Do **not** evaluate SpEL on user input (e.g., via `SpelExpressionParser.parseExpression(userInput)` - or any dynamic expression features wired to request parameters). - - Always use fixed or strictly whitelisted view names in controllers (`return "fixedView"`), - never build view names or fragment expressions from request data. - - Configure template engines in “safe” modes where possible: restrict method invocation, - access to arbitrary classes, and reflection in SpEL/OGNL. - - Escape user-supplied text appropriately in templates (e.g., `th:text` instead of `th:utext` in Thymeleaf) - to prevent it from being interpreted as HTML/JavaScript. - - Keep Spring, Thymeleaf, and other template engine libraries up-to-date to benefit from security fixes and safer defaults. - references: - - https://portswigger.net/web-security/server-side-template-injection - provenance: https://github.com/github/codeql/tree/cdd8aa49e16650a96b8993e8745c7672600fe930/java/ql/lib/ext - languages: - - java - mode: join - join: - refs: + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/template-injection-sinks.yaml#java-ssti-sinks as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - id: el-injection-in-servlet-app diff --git a/rules/java/security/command-injection.yaml b/rules/java/security/command-injection.yaml index 777986f..750a681 100644 --- a/rules/java/security/command-injection.yaml +++ b/rules/java/security/command-injection.yaml @@ -1,5 +1,5 @@ rules: - - id: os-command-injection-in-servlet-app + - id: os-command-injection severity: ERROR message: >- Potential OS command injection: command line depends on a user provided value @@ -7,214 +7,46 @@ rules: cwe: CWE-78 short-description: Command line depends on a user provided value full-description: |- - Operating System (OS) Command Injection is a vulnerability that occurs when an application - constructs and executes system-level commands using untrusted input. In Java, this commonly - appears when user-supplied data is concatenated into a command string passed to `Runtime.exec()` - or `ProcessBuilder`. If that input is not strictly validated or constrained, an attacker can - append additional commands or alter command arguments, leading to arbitrary command execution - with the privileges of the running Java process. This can result in data exfiltration, - server compromise, lateral movement, or complete takeover of the underlying host. + Command injection occurs when untrusted data reaches process-execution APIs such as + `Runtime.exec(...)` or `ProcessBuilder.command(...)`. - Vulnerable code sample - ```java - import javax.servlet.http.HttpServlet; - import javax.servlet.http.HttpServletRequest; - import javax.servlet.http.HttpServletResponse; - - public class PingServlet extends HttpServlet { - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) { - String host = request.getParameter("host"); // e.g. user-controlled input - - // VULNERABLE: direct concatenation of untrusted input into OS command - String command = "ping -c 4 " + host; - - try { - Process process = Runtime.getRuntime().exec(command); - // ... read output, return to user, etc. - } catch (Exception e) { - // handle exception - } - } - } - ``` - - If an attacker sends `host=example.com; rm -rf /` (or platform-specific equivalents), - and the environment uses a shell to interpret `command`, the injected `rm -rf /` may be executed. + Vulnerable example: - To remediate this issue, avoid constructing shell commands with untrusted input and prefer safer APIs - that do not rely on a shell. If you must invoke external programs, pass arguments as discrete parameters - (not a single concatenated string), and strictly validate or whitelist allowed values. - - Safer approach 1 — use Java APIs instead of shell commands: ```java - import javax.servlet.http.HttpServlet; - import javax.servlet.http.HttpServletRequest; - import javax.servlet.http.HttpServletResponse; - import java.net.InetAddress; - - public class PingServlet extends HttpServlet { - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) { - String host = request.getParameter("host"); + import java.io.IOException; - try { - // Basic validation (example: allow only hostnames/IPs with limited charset) - if (!host.matches("^[a-zA-Z0-9._-]{1,255}$")) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid host"); - return; - } + public class CommandService { + public Process runPing(String hostFromRequest) throws IOException { + // hostFromRequest is untrusted + String cmd = "ping -c 4 " + hostFromRequest; - InetAddress address = InetAddress.getByName(host); - boolean reachable = address.isReachable(5000); // 5 seconds timeout - - response.getWriter().println("Reachable: " + reachable); - } catch (Exception e) { - // handle exception - } + // VULNERABLE + return Runtime.getRuntime().exec(cmd); } } ``` - Safer approach 2 — if you must call an OS command, avoid a shell and separate arguments: - ```java - String host = request.getParameter("host"); - - // Strict validation / whitelisting - if (!host.matches("^[a-zA-Z0-9._-]{1,255}$")) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid host"); - return; - } - - // Use ProcessBuilder with arguments array; do not invoke /bin/sh or cmd.exe - ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", host); - pb.redirectErrorStream(true); - Process process = pb.start(); - // ... safely read output - ``` - - Key remediation steps: - - Do not concatenate untrusted input into command strings. - - Prefer native Java libraries over external commands where possible. - - If external commands are unavoidable: - - Avoid invoking a shell (`/bin/sh -c`, `cmd.exe /c`). - - Pass arguments as a list (`new ProcessBuilder("cmd", "arg1", "arg2")`), not a single string. - - Apply strict input validation and whitelisting (fixed set of allowed commands/arguments when feasible). - - Run the Java process with the least privileges necessary and apply OS-level hardening to reduce impact. - references: - - https://owasp.org/www-community/attacks/Command_Injection - - https://cwe.mitre.org/data/definitions/78.html - license: MIT - provenance: https://find-sec-bugs.github.io/bugs.htm#COMMAND_INJECTION - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/command-injection-sinks.yaml#command-injection-sinks - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: os-command-injection-in-spring-app - severity: ERROR - message: >- - Potential OS command injection: command line depends on a user provided value - metadata: - cwe: CWE-78 - short-description: Command line depends on a user provided value - full-description: |- - OS Command Injection in a Spring application occurs when a controller or service uses untrusted HTTP input (from query parameters, path variables, request bodies, headers, etc.) to build and execute operating system commands. Because Spring automatically maps request data into method parameters, it's easy to accidentally pass user-controlled strings directly into `Runtime.exec()` or `ProcessBuilder`. If that input is not strictly validated or constrained, an attacker can inject additional commands or modify arguments, leading to arbitrary command execution with the privileges of the running Spring application. + Safe example: - Vulnerable code sample ```java - import org.springframework.web.bind.annotation.GetMapping; - import org.springframework.web.bind.annotation.RequestParam; - import org.springframework.web.bind.annotation.RestController; - - import java.io.BufferedReader; - import java.io.InputStreamReader; + import java.io.IOException; - @RestController - public class PingController { - - @GetMapping("/ping") - public String ping(@RequestParam String host) { - // VULNERABLE: direct concatenation of untrusted input into OS command - String command = "ping -c 4 " + host; - - StringBuilder output = new StringBuilder(); - try { - Process process = Runtime.getRuntime().exec(command); // OS command injection sink - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append('\n'); - } - } - } catch (Exception e) { - return "Error: " + e.getMessage(); + public class CommandService { + public Process runPing(String hostFromRequest) throws IOException { + if (hostFromRequest == null || !hostFromRequest.matches("^[A-Za-z0-9.-]{1,255}$")) { + throw new IllegalArgumentException("Invalid host"); } - return output.toString(); - } - } - ``` - - To remediate this issue, avoid building shell commands from user input and prefer Java APIs or safe process invocation patterns. In Spring specifically, combine safer APIs with strong validation at the controller boundary. - - Safer approach — validate input and avoid the shell - ```java - import jakarta.validation.constraints.Pattern; - import org.springframework.validation.annotation.Validated; - import org.springframework.web.bind.annotation.*; - - import java.io.BufferedReader; - import java.io.InputStreamReader; - - @RestController - @Validated - public class SafePingController { - - @GetMapping("/ping") - public String ping( - @RequestParam - @Pattern(regexp = "^[a-zA-Z0-9._-]{1,255}$", message = "Invalid host") - String host) { - // Prefer Java APIs (e.g., InetAddress) where possible. - // If you must call an OS command, use ProcessBuilder with separated args. - StringBuilder output = new StringBuilder(); - try { - ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", host); - pb.redirectErrorStream(true); - Process process = pb.start(); - - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append('\n'); - } - } - } catch (Exception e) { - return "Error: " + e.getMessage(); - } - return output.toString(); + // Safe usage: fixed executable + separated arguments + ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", hostFromRequest); + pb.redirectErrorStream(true); + return pb.start(); } } ``` - Key points in Spring: - - Do not concatenate request parameters, path variables, or body values into command strings. - - Prefer Java libraries (e.g., `InetAddress.isReachable`) instead of calling OS utilities where feasible. - - If external commands are unavoidable: - - Use `ProcessBuilder` with argument lists, not a single shell-processed string. - - Do not invoke a shell (`/bin/sh -c`, `cmd.exe /c`). - - Apply strong validation/whitelisting at the controller layer (`@Validated`, `@Pattern`, custom validators). - - Run the Spring application with least-privilege OS accounts and use defense-in-depth (containerization, AppArmor/SELinux, etc.) to minimize impact. + Key vulnerable patterns covered by this rule include `Runtime.exec/load/loadLibrary` and + `ProcessBuilder` command construction with tainted arguments. references: - https://owasp.org/www-community/attacks/Command_Injection - https://cwe.mitre.org/data/definitions/78.html @@ -225,9 +57,12 @@ rules: mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/command-injection-sinks.yaml#command-injection-sinks as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' diff --git a/rules/java/security/crlf-injection.yaml b/rules/java/security/crlf-injection.yaml index 5eb7dde..fbaee4a 100644 --- a/rules/java/security/crlf-injection.yaml +++ b/rules/java/security/crlf-injection.yaml @@ -1,5 +1,5 @@ rules: - - id: http-response-splitting-in-servlet-app + - id: http-response-splitting severity: WARNING message: >- Older Java application servers are vulnerable to HTTP response splitting, which @@ -9,271 +9,37 @@ rules: - CWE-113 short-description: HTTP response splitting full-description: |- - HTTP request/response splitting is a class of HTTP header injection vulnerabilities that occurs when untrusted input is inserted into HTTP headers without proper validation or encoding. An attacker can inject carriage return (`\r`) and line feed (`\n`) characters (CRLF) into header values, prematurely terminating one HTTP request/response and starting another. In Java web applications (Servlets, JSP, Spring MVC, etc.), this typically happens when user-controlled data is written directly into HTTP headers (including `Location` for redirects) without sanitization, enabling cache poisoning, cross-site scripting, and request smuggling/splitting attacks. + HTTP response splitting occurs when untrusted input reaches response-header APIs + (`setHeader`, `addHeader`, cookie value operations) without CR/LF neutralization. - ```java - // Vulnerable code sample - - import jakarta.servlet.ServletException; - import jakarta.servlet.http.HttpServlet; - import jakarta.servlet.http.HttpServletRequest; - import jakarta.servlet.http.HttpServletResponse; - - import java.io.IOException; - - public class ProfileServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - - // Attacker controls the 'user' parameter, e.g.: - // ?user=alice%0D%0ASet-Cookie:%20session=attacker - String user = request.getParameter("user"); - - // VULNERABLE: unvalidated user input goes directly into an HTTP header - response.setHeader("X-User", user); - - // Another common pattern: unsafe redirect parameter - // e.g. ?next=%0D%0ASet-Cookie:%20session=attacker - String next = request.getParameter("next"); - response.sendRedirect("/home?next=" + next); - - // If 'user' or 'next' contain CRLF sequences, the attacker can - // inject additional headers or even a second HTTP response. - } - } - ``` - - To remediate this issue, never write untrusted input directly into HTTP header fields (including `Location` for redirects) without strict validation and encoding: - - 1. **Reject CR (`\r`) and LF (`\n`) characters in any value that may end up in a header.** - 2. **Validate header-related input with a whitelist (allow-list) of safe characters and/or specific patterns.** For example, language codes, tokens, or IDs should match a narrow regex. - 3. **For URLs and query parameters, use proper URL encoding** (`URLEncoder`) instead of concatenation. - 4. **Prefer framework helpers that enforce invariants**, such as `HttpServletResponse.encodeRedirectURL(...)`, and avoid exposing raw header manipulation to untrusted values. - 5. **Upgrade and configure your servlet container / app server** (Tomcat, Jetty, etc.) to ensure it rejects illegal CRLF in headers, but do not rely on this as the only defense. - - Safe example with validation and encoding: - - ```java - import jakarta.servlet.ServletException; - import jakarta.servlet.http.HttpServlet; - import jakarta.servlet.http.HttpServletRequest; - import jakarta.servlet.http.HttpServletResponse; - - import java.io.IOException; - import java.net.URLEncoder; - import java.nio.charset.StandardCharsets; - - public class SafeProfileServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - - String user = request.getParameter("user"); - if (user == null) { - user = "anonymous"; - } - - // 1. Reject CR/LF explicitly - if (user.contains("\r") || user.contains("\n")) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid user"); - return; - } - - // 2. Optionally enforce a strict pattern for header-safe data - if (!user.matches("^[A-Za-z0-9_-]{1,32}$")) { - user = "anonymous"; // or reject the request - } - - // SAFE: header value is validated and free of CRLF - response.setHeader("X-User", user); - - // 3. When building URLs, encode untrusted input as a query parameter - String next = request.getParameter("next"); - if (next == null) { - next = "/"; - } - - // Reject CR/LF and other dangerous patterns if you later interpret 'next' - if (next.contains("\r") || next.contains("\n")) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid next parameter"); - return; - } - - String encodedNext = URLEncoder.encode(next, StandardCharsets.UTF_8); - String redirectUrl = response.encodeRedirectURL("/home?next=" + encodedNext); - response.sendRedirect(redirectUrl); - } - } - ``` - references: - - https://owasp.org/www-community/attacks/HTTP_Response_Splitting - - https://en.wikipedia.org/wiki/HTTP_response_splitting - license: MIT - provenance: https://gitlab.com/gitlab-org/security-products/sast-rules/-/blob/main/java/cookie/rule-HttpResponseSplitting.yml - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/http-response-splitting-sinks.yaml#java-http-response-splitting-sink - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: http-response-splitting-in-spring-app - severity: WARNING - message: >- - Older Java application servers are vulnerable to HTTP response splitting, which - may occur if an HTTP request can be injected with CRLF characters. - metadata: - cwe: - - CWE-113 - short-description: HTTP response splitting - full-description: |- - HTTP request/response splitting (often referred to as HTTP response splitting) is an HTTP header injection vulnerability that occurs when untrusted input is placed into HTTP headers without proper validation or encoding. An attacker can inject carriage return (`\r`) and line feed (`\n`) characters (CRLF) into header values to terminate the current header section and start a new header or response. - In Spring MVC / Spring Boot applications, this typically happens when user-controlled data is used directly in: - - - Response headers (via `HttpServletResponse` or `ResponseEntity`) - - Redirect URLs (e.g. using `"redirect:" + userInput`) - - allowing an attacker to poison caches, inject responses, or perform XSS via crafted HTTP responses. + Vulnerable example: ```java - // Vulnerable code sample — Spring MVC / Spring Boot - - package com.example.demo; - - import jakarta.servlet.http.HttpServletResponse; - import org.springframework.stereotype.Controller; - import org.springframework.web.bind.annotation.GetMapping; - import org.springframework.web.bind.annotation.RequestParam; - - import java.io.IOException; - - @Controller - public class ProfileController { - - // Example 1: header injection - @GetMapping("/profile") - public void profile(@RequestParam(name = "user", required = false) String user, - HttpServletResponse response) throws IOException { - - // Attacker-supplied: ?user=alice%0D%0ASet-Cookie:%20session=attacker - if (user == null) { - user = "anonymous"; - } - - // VULNERABLE: unvalidated input used directly as header value - response.setHeader("X-User", user); + import javax.servlet.http.HttpServletResponse; - response.getWriter().write("Profile"); - } - - // Example 2: unsafe redirect - @GetMapping("/go") - public String redirect(@RequestParam("next") String next) { - - // Attacker-supplied: - // ?next=%0D%0ASet-Cookie:%20session=attacker - // or more complex payloads - // - // VULNERABLE in designs/versions that don't sanitize CRLF properly: - // user input is concatenated into the redirect target, which will be - // used as the Location header. - return "redirect:" + next; + public class HeaderService { + public void writeHeader(HttpServletResponse response, String valueFromRequest) { + // valueFromRequest is untrusted + response.setHeader("X-User", valueFromRequest); // VULNERABLE } } ``` - To remediate this issue, validate and sanitize all untrusted data before using it in HTTP headers or redirect URLs, and rely on safe builders/encoders instead of string concatenation: - - 1. **Never place raw user input into headers** (`setHeader`, `addHeader`, `ResponseEntity.header`, or `redirect:` URL fragments). - 2. **Explicitly reject CR (`\r`) and LF (`\n`) in all values that might be used in headers or redirect URLs.** - 3. **Use strict allow-lists / regex validation** for header and redirect parameters (e.g. only allow alphanumerics and a small set of safe characters). - 4. **Build URLs using Spring utilities** such as `UriComponentsBuilder`, which properly encodes query parameters rather than concatenating strings. - 5. **Keep Spring Framework and your servlet container up to date** so built‑in CRLF defenses are present, but do not rely on them as the only safeguard. - Safe example: ```java - package com.example.demo; - - import jakarta.servlet.http.HttpServletResponse; - import org.springframework.stereotype.Controller; - import org.springframework.web.bind.annotation.GetMapping; - import org.springframework.web.bind.annotation.RequestParam; - import org.springframework.web.util.UriComponentsBuilder; - - import java.io.IOException; - import java.nio.charset.StandardCharsets; - - @Controller - public class SafeProfileController { - - @GetMapping("/profile") - public void profile(@RequestParam(name = "user", required = false) String user, - HttpServletResponse response) throws IOException { - - if (user == null) { - user = "anonymous"; - } - - // 1. Reject CR/LF to prevent header injection - if (user.contains("\r") || user.contains("\n")) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid user"); - return; - } - - // 2. Enforce a strict pattern for header-safe values - if (!user.matches("^[A-Za-z0-9_-]{1,32}$")) { - user = "anonymous"; // or reject the request instead - } + import javax.servlet.http.HttpServletResponse; - // SAFE: header value is validated and free from CRLF - response.setHeader("X-User", user); - - response.getWriter().write("Profile"); - } - - @GetMapping("/go") - public String safeRedirect(@RequestParam(name = "next", required = false) String next) { - - if (next == null || next.isBlank()) { - next = "/home"; - } - - // 3. Reject CR/LF in redirect target - if (next.contains("\r") || next.contains("\n")) { - // In a real app, log and redirect to a safe default - next = "/home"; - } - - // 4. Optionally, enforce a stricter rule: only allow local paths - if (!next.startsWith("/")) { - next = "/home"; - } - - // 5. Build and encode the redirect URL safely - String url = UriComponentsBuilder - .fromPath(next) - // Example of adding an encoded query parameter: - // .queryParam("ref", "someRef") - .build() - .encode(StandardCharsets.UTF_8) - .toUriString(); - - // SAFE: Spring will use the encoded URL as the Location header - return "redirect:" + url; + public class HeaderService { + public void writeHeader(HttpServletResponse response, String valueFromRequest) { + String safe = valueFromRequest == null ? "" : valueFromRequest.replaceAll("[\\r\\n]", ""); + response.setHeader("X-User", safe); } } ``` + + Key vulnerable patterns covered by this rule include `HttpServletResponse.setHeader/addHeader`, + response-wrapper header operations, and cookie value construction with tainted data. references: - https://owasp.org/www-community/attacks/HTTP_Response_Splitting - https://en.wikipedia.org/wiki/HTTP_response_splitting @@ -284,14 +50,17 @@ rules: mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/http-response-splitting-sinks.yaml#java-http-response-splitting-sink as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - id: java-servlet-smtp-crlf-injection + - id: smtp-crlf-injection severity: ERROR message: >- Potential CRLF injection into SMTP message: @@ -305,300 +74,37 @@ rules: cwe: CWE-77 short-description: CRLF injection into SMTP message full-description: |- - CRLF (Carriage Return + Line Feed, `\r\n`) injection into an SMTP `MimeMessage` occurs when untrusted input is placed directly into email headers (e.g., `Subject`, `To`, `From`, custom headers) without properly validating or stripping newline characters. - - Because SMTP and MIME use `\r\n` to separate headers and the message body, an attacker who can inject `\r\n` into a header field can prematurely terminate that header and start injecting new headers or even alter the body. This can be abused to: - - - Add unintended recipients (`Bcc`, `Cc`, additional `To`). - - Spoof or modify headers (e.g., `From`, `Reply-To`, `X-...`). - - Manipulate message content or spam/abuse mail infrastructure. - - In Java, this often happens when using `MimeMessage` (e.g., via Jakarta Mail / JavaMail) with user-supplied values for headers without sanitization, or when bypassing built-in validation by using low-level header methods. - - --- - - Vulnerable code sample - - ```java - public void sendEmail(HttpServletRequest request) throws MessagingException { - String to = request.getParameter("to"); - String subject = request.getParameter("subject"); - String body = request.getParameter("message"); - - Session session = getMailSession(); - MimeMessage message = new MimeMessage(session); - - // User-controlled values, no validation - message.setFrom(new InternetAddress("noreply@example.com")); - message.setRecipient(Message.RecipientType.TO, new InternetAddress(to)); - - // Vulnerable: subject comes directly from user input - // If subject contains "\r\nBcc: victim@example.com", that may create a new header line - message.setHeader("Subject", subject); // bad: manual header setting - - // Also dangerous when using generic headers - String trackingId = request.getParameter("trackingId"); - message.setHeader("X-Tracking-Id", trackingId); // unvalidated header value - - message.setText(body); - - Transport.send(message); - } - ``` + SMTP header injection occurs when untrusted input reaches `MimeMessage` header-related APIs + (`setSubject`, `setHeader`, `addHeader`, and similar methods). - If an attacker supplies a value like: - - ```text - subject = "Hello\r\nBcc: attacker@example.com" - ``` - - and the underlying library does not strip or reject CR/LF characters for that header-setting method, the generated email may contain an injected `Bcc` header, silently copying all messages to the attacker. - - --- - - To remediate this issue, ensure that any user-controlled input used in email headers is strictly validated and cannot contain CR (`\r`) or LF (`\n`) characters, and avoid low-level header methods where safer, higher-level APIs exist. - - Key steps: - - 1. **Disallow CR and LF in header fields** - Strip or reject any input that contains `\r` or `\n` if it will be used in a header (e.g., `Subject`, `From`, `To`, `Reply-To`, custom `X-*` headers). - - 2. **Use high-level `MimeMessage` APIs** - Prefer methods like `setSubject`, `setFrom`, `setRecipients`, which usually perform syntax checks, instead of `setHeader` / `addHeader` with raw values. Do not manually craft header lines. - - 3. **Apply strict validation** - - For email addresses: validate against a reasonable email regex and/or let `InternetAddress` parse and validate addresses, and reject invalid ones. - - For subjects / custom header values: define allowed character sets or patterns (e.g., letters, numbers, spaces, basic punctuation) and reject anything else. - - 4. **Keep your mail library up to date** - Use the latest Jakarta Mail / JavaMail version, as many unsafe behaviors (including header validation) have been tightened over time. But even with updated libraries, you should still validate input. - - Safe code sample: + Vulnerable example: ```java - public void sendEmail(HttpServletRequest request) throws MessagingException { - String toParam = request.getParameter("to"); - String subjectParam = request.getParameter("subject"); - String bodyParam = request.getParameter("message"); - String trackingIdParam = request.getParameter("trackingId"); - - // Basic null checks omitted for brevity - - // 1. Validate and sanitize inputs + import jakarta.mail.internet.MimeMessage; - // Reject CR and LF in any header-bound fields - if (containsCRLF(subjectParam) || containsCRLF(trackingIdParam) || containsCRLF(toParam)) { - throw new IllegalArgumentException("Invalid input"); + public class MailService { + public void applySubject(MimeMessage msg, String subjectFromRequest) throws Exception { + // subjectFromRequest is untrusted + msg.setHeader("Subject", subjectFromRequest); // VULNERABLE } - - // Validate email address format via InternetAddress - InternetAddress toAddress; - try { - toAddress = new InternetAddress(toParam, true); // 'true' for strict validation - } catch (AddressException ex) { - throw new IllegalArgumentException("Invalid recipient address", ex); - } - - // Optionally, normalize subject to a safe subset of characters - String safeSubject = subjectParam.replaceAll("[\r\n]", "").trim(); - - // Optionally, validate custom header (e.g., alphanumeric + dash only) - String safeTrackingId = trackingIdParam.replaceAll("[^A-Za-z0-9\\-]", ""); - - // 2. Use higher-level MimeMessage APIs - - Session session = getMailSession(); - MimeMessage message = new MimeMessage(session); - - message.setFrom(new InternetAddress("noreply@example.com")); - message.setRecipient(Message.RecipientType.TO, toAddress); - - // Use setSubject, which encodes and validates header correctly - message.setSubject(safeSubject, "UTF-8"); - - // Custom headers: still only after sanitization - if (!safeTrackingId.isEmpty()) { - message.setHeader("X-Tracking-Id", safeTrackingId); - } - - message.setText(bodyParam, "UTF-8"); - - Transport.send(message); - } - - private boolean containsCRLF(String value) { - return value != null && (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0); - } - ``` - - In this remediated version: - - - All header-bound values are checked for `\r` and `\n`. - - Email address is validated via `InternetAddress`. - - `setSubject` is used instead of `setHeader("Subject", …)`. - - Custom header is sanitized to a safe character set before being used. - references: - - https://owasp.org/www-community/vulnerabilities/CRLF_Injection - - https://owasp.org/www-community/attacks/Email_Injection - provenance: https://gitlab.com/gitlab-org/security-products/sast-rules/-/blob/main/java/smtp/rule-InsecureSmtp.yml - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/smtp-injection-sinks.yaml#java-smtp-crlf-injection-sinks - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: spring-smtp-crlf-injection - severity: ERROR - message: >- - Potential CRLF injection into SMTP message: - The application was found calling `MimeMessage` methods without encoding - new line characters. Much like HTTP, Simple Mail Transfer Protocol (SMTP) is a - text based protocol that uses headers to convey additional directives for how - email messages should be treated. An adversary could potentially cause email - messages to be sent to unintended recipients by abusing the CC or BCC headers - if they were able to inject them. - metadata: - cwe: CWE-77 - short-description: CRLF injection into SMTP message - full-description: |- - CRLF (Carriage Return + Line Feed, `\r\n`) injection into an SMTP `MimeMessage` occurs when untrusted input is placed directly into email headers (e.g., `Subject`, `To`, `From`, custom headers) without properly validating or stripping newline characters. - - Because SMTP and MIME use `\r\n` to separate headers and the message body, an attacker who can inject `\r\n` into a header field can prematurely terminate that header and start injecting new headers or even alter the body. This can be abused to: - - - Add unintended recipients (`Bcc`, `Cc`, additional `To`). - - Spoof or modify headers (e.g., `From`, `Reply-To`, `X-...`). - - Manipulate message content or spam/abuse mail infrastructure. - - In Java, this often happens when using `MimeMessage` (e.g., via Jakarta Mail / JavaMail) with user-supplied values for headers without sanitization, or when bypassing built-in validation by using low-level header methods. - - --- - - Vulnerable code sample - - ```java - public void sendEmail(HttpServletRequest request) throws MessagingException { - String to = request.getParameter("to"); - String subject = request.getParameter("subject"); - String body = request.getParameter("message"); - - Session session = getMailSession(); - MimeMessage message = new MimeMessage(session); - - // User-controlled values, no validation - message.setFrom(new InternetAddress("noreply@example.com")); - message.setRecipient(Message.RecipientType.TO, new InternetAddress(to)); - - // Vulnerable: subject comes directly from user input - // If subject contains "\r\nBcc: victim@example.com", that may create a new header line - message.setHeader("Subject", subject); // bad: manual header setting - - // Also dangerous when using generic headers - String trackingId = request.getParameter("trackingId"); - message.setHeader("X-Tracking-Id", trackingId); // unvalidated header value - - message.setText(body); - - Transport.send(message); } ``` - If an attacker supplies a value like: - - ```text - subject = "Hello\r\nBcc: attacker@example.com" - ``` - - and the underlying library does not strip or reject CR/LF characters for that header-setting method, the generated email may contain an injected `Bcc` header, silently copying all messages to the attacker. - - --- - - To remediate this issue, ensure that any user-controlled input used in email headers is strictly validated and cannot contain CR (`\r`) or LF (`\n`) characters, and avoid low-level header methods where safer, higher-level APIs exist. - - Key steps: - - 1. **Disallow CR and LF in header fields** - Strip or reject any input that contains `\r` or `\n` if it will be used in a header (e.g., `Subject`, `From`, `To`, `Reply-To`, custom `X-*` headers). - - 2. **Use high-level `MimeMessage` APIs** - Prefer methods like `setSubject`, `setFrom`, `setRecipients`, which usually perform syntax checks, instead of `setHeader` / `addHeader` with raw values. Do not manually craft header lines. - - 3. **Apply strict validation** - - For email addresses: validate against a reasonable email regex and/or let `InternetAddress` parse and validate addresses, and reject invalid ones. - - For subjects / custom header values: define allowed character sets or patterns (e.g., letters, numbers, spaces, basic punctuation) and reject anything else. - - 4. **Keep your mail library up to date** - Use the latest Jakarta Mail / JavaMail version, as many unsafe behaviors (including header validation) have been tightened over time. But even with updated libraries, you should still validate input. - - Safe code sample: + Safe example: ```java - public void sendEmail(HttpServletRequest request) throws MessagingException { - String toParam = request.getParameter("to"); - String subjectParam = request.getParameter("subject"); - String bodyParam = request.getParameter("message"); - String trackingIdParam = request.getParameter("trackingId"); + import jakarta.mail.internet.MimeMessage; - // Basic null checks omitted for brevity - - // 1. Validate and sanitize inputs - - // Reject CR and LF in any header-bound fields - if (containsCRLF(subjectParam) || containsCRLF(trackingIdParam) || containsCRLF(toParam)) { - throw new IllegalArgumentException("Invalid input"); - } - - // Validate email address format via InternetAddress - InternetAddress toAddress; - try { - toAddress = new InternetAddress(toParam, true); // 'true' for strict validation - } catch (AddressException ex) { - throw new IllegalArgumentException("Invalid recipient address", ex); + public class MailService { + public void applySubject(MimeMessage msg, String subjectFromRequest) throws Exception { + String safe = subjectFromRequest == null ? "" : subjectFromRequest.replaceAll("[\\r\\n]", ""); + msg.setSubject(safe, "UTF-8"); } - - // Optionally, normalize subject to a safe subset of characters - String safeSubject = subjectParam.replaceAll("[\r\n]", "").trim(); - - // Optionally, validate custom header (e.g., alphanumeric + dash only) - String safeTrackingId = trackingIdParam.replaceAll("[^A-Za-z0-9\\-]", ""); - - // 2. Use higher-level MimeMessage APIs - - Session session = getMailSession(); - MimeMessage message = new MimeMessage(session); - - message.setFrom(new InternetAddress("noreply@example.com")); - message.setRecipient(Message.RecipientType.TO, toAddress); - - // Use setSubject, which encodes and validates header correctly - message.setSubject(safeSubject, "UTF-8"); - - // Custom headers: still only after sanitization - if (!safeTrackingId.isEmpty()) { - message.setHeader("X-Tracking-Id", safeTrackingId); - } - - message.setText(bodyParam, "UTF-8"); - - Transport.send(message); - } - - private boolean containsCRLF(String value) { - return value != null && (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0); } ``` - In this remediated version: - - - All header-bound values are checked for `\r` and `\n`. - - Email address is validated via `InternetAddress`. - - `setSubject` is used instead of `setHeader("Subject", …)`. - - Custom header is sanitized to a safe character set before being used. + Key vulnerable patterns covered by this rule include `MimeMessage.setSubject`, `setHeader`, `addHeader`, + `setDescription`, and `setDisposition` with tainted data. references: - https://owasp.org/www-community/vulnerabilities/CRLF_Injection - https://owasp.org/www-community/attacks/Email_Injection @@ -608,9 +114,12 @@ rules: mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/smtp-injection-sinks.yaml#java-smtp-crlf-injection-sinks as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' diff --git a/rules/java/security/data-query-injection.yaml b/rules/java/security/data-query-injection.yaml index 9dcf6fb..bf9a753 100644 --- a/rules/java/security/data-query-injection.yaml +++ b/rules/java/security/data-query-injection.yaml @@ -1,5 +1,5 @@ rules: - - id: xpath-injection-in-servlet-app + - id: xpath-injection severity: ERROR message: >- Potential XPath injection: detected input from a HTTPServletRequest going into a XPath evaluate or compile command. @@ -7,200 +7,49 @@ rules: cwe: CWE-643 short-description: XPath Injection full-description: |- - XPath Injection in Java occurs when untrusted data is concatenated into an XPath expression - that is then evaluated on an XML document. An attacker can inject XPath syntax - (such as additional predicates or logical operators) to change the query's meaning, - bypass authentication or authorization checks, or access sensitive data. - It is conceptually similar to SQL injection, but targets XML data sources and - XPath processors instead of relational databases. + XPath injection happens when untrusted input reaches XPath evaluation APIs as expression text + (for example `XPath.compile(...)` or `XPath.evaluate(...)`). - ```java - // Vulnerable code sample - import javax.xml.xpath.*; - import org.w3c.dom.Document; - - public class AuthService { - - private final XPath xPath; - private final Document usersDoc; // XML like: ...... - - public AuthService(Document usersDoc) { - this.usersDoc = usersDoc; - this.xPath = XPathFactory.newInstance().newXPath(); - } - - public boolean authenticate(String username, String password) throws Exception { - // VULNERABLE: user input is concatenated directly into the XPath expression - String expression = "/users/user[username='" + username + "' and password='" + password + "']"; - - // If attacker sets username to: ' or '1'='1 - // the expression becomes: /users/user[username='' or '1'='1' and password='...'] - // which can match any user and bypass auth. - XPathExpression compiled = xPath.compile(expression); - Object result = compiled.evaluate(usersDoc, XPathConstants.NODE); - return result != null; - } - } - ``` - - To remediate this issue, avoid building XPath expressions via string concatenation with untrusted input, - validate and constrain any user input used in queries, and prefer designs where comparisons are done in code - rather than in dynamically built XPath. For example, instead of injecting untrusted data into the query, - select candidate nodes with a static XPath and compare values in Java: - - ```java - // Safer approach: do not concatenate untrusted input into the XPath - import javax.xml.xpath.*; - import org.w3c.dom.*; - - public class SafeAuthService { - - private final XPath xPath; - private final Document usersDoc; - - public SafeAuthService(Document usersDoc) { - this.usersDoc = usersDoc; - this.xPath = XPathFactory.newInstance().newXPath(); - } - - public boolean authenticate(String username, String password) throws Exception { - // 1. Validate input (length, charset, etc.) — example: - if (username == null || password == null - || username.length() > 50 || password.length() > 100 - || !username.matches("[A-Za-z0-9._-]+")) { - return false; - } - - // 2. Use a static XPath to get all user nodes (no user input inside XPath) - XPathExpression compiled = xPath.compile("/users/user"); - NodeList users = (NodeList) compiled.evaluate(usersDoc, XPathConstants.NODESET); - - // 3. Compare in code instead of in XPath - for (int i = 0; i < users.getLength(); i++) { - Element user = (Element) users.item(i); - String u = user.getElementsByTagName("username").item(0).getTextContent(); - String p = user.getElementsByTagName("password").item(0).getTextContent(); - if (username.equals(u) && password.equals(p)) { - return true; - } - } - return false; - } - } - ``` - - In addition, prefer using standard, hardened authentication and data-storage mechanisms - (e.g., databases with parameterized queries, hashed passwords) instead of custom XML-based auth; - if XPath must be used, restrict allowed characters, escape or encode any user-controlled values - before inserting them into expressions, and keep XPath expressions as static as possible. - references: - - https://owasp.org/www-community/attacks/XPATH_Injection - - https://cheatsheetseries.owasp.org/cheatsheets/XML_Security_Cheat_Sheet.html - provenance: https://github.com/semgrep/semgrep-rules/blob/develop/java/lang/security/audit/tainted-xpath-from-http-request.yaml - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/data-query-injection-sinks.yaml#java-xpath-injection-sinks - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: xpath-injection-in-spring-app - severity: ERROR - message: >- - Potential XPath injection: detected input from a HTTP request going into a XPath evaluate or compile command. - metadata: - cwe: CWE-643 - short-description: XPath Injection - full-description: |- - XPath Injection in Java occurs when untrusted data is concatenated into an XPath expression that is then evaluated on an XML document. - An attacker can inject XPath syntax (such as additional predicates or logical operators) to change the query's meaning, - bypass authentication or authorization checks, or access sensitive data. It is conceptually similar to SQL injection, - but targets XML data sources and XPath processors instead of relational databases. + Vulnerable example: ```java - // Vulnerable code sample - import javax.xml.xpath.*; - import org.w3c.dom.Document; - - public class AuthService { - - private final XPath xPath; - private final Document usersDoc; // XML like: ...... - - public AuthService(Document usersDoc) { - this.usersDoc = usersDoc; - this.xPath = XPathFactory.newInstance().newXPath(); - } + import javax.xml.xpath.XPath; + import javax.xml.xpath.XPathFactory; - public boolean authenticate(String username, String password) throws Exception { - // VULNERABLE: user input is concatenated directly into the XPath expression - String expression = "/users/user[username='" + username + "' and password='" + password + "']"; + public class XmlLookupService { + private final XPath xp = XPathFactory.newInstance().newXPath(); - // If attacker sets username to: ' or '1'='1 - // the expression becomes: /users/user[username='' or '1'='1' and password='...'] - // which can match any user and bypass auth. - XPathExpression compiled = xPath.compile(expression); - Object result = compiled.evaluate(usersDoc, XPathConstants.NODE); - return result != null; + public String lookup(String exprFromRequest, org.w3c.dom.Document doc) throws Exception { + // exprFromRequest is untrusted + return xp.evaluate(exprFromRequest, doc); // VULNERABLE } } ``` - To remediate this issue, avoid building XPath expressions via string concatenation with untrusted input, - validate and constrain any user input used in queries, and prefer designs where comparisons are done in code - rather than in dynamically built XPath. For example, instead of injecting untrusted data into the query, - select candidate nodes with a static XPath and compare values in Java: + Safe example: ```java - // Safer approach: do not concatenate untrusted input into the XPath - import javax.xml.xpath.*; - import org.w3c.dom.*; + import javax.xml.xpath.XPath; + import javax.xml.xpath.XPathConstants; + import javax.xml.xpath.XPathExpression; + import javax.xml.xpath.XPathFactory; - public class SafeAuthService { + public class XmlLookupService { + private final XPath xp = XPathFactory.newInstance().newXPath(); - private final XPath xPath; - private final Document usersDoc; - - public SafeAuthService(Document usersDoc) { - this.usersDoc = usersDoc; - this.xPath = XPathFactory.newInstance().newXPath(); - } - - public boolean authenticate(String username, String password) throws Exception { - // 1. Validate input (length, charset, etc.) — example: - if (username == null || password == null - || username.length() > 50 || password.length() > 100 - || !username.matches("[A-Za-z0-9._-]+")) { - return false; + public org.w3c.dom.Node findUserById(String userIdFromRequest, org.w3c.dom.Document doc) throws Exception { + if (userIdFromRequest == null || !userIdFromRequest.matches("[A-Za-z0-9_-]{1,32}")) { + throw new IllegalArgumentException("Invalid user id"); } - // 2. Use a static XPath to get all user nodes (no user input inside XPath) - XPathExpression compiled = xPath.compile("/users/user"); - NodeList users = (NodeList) compiled.evaluate(usersDoc, XPathConstants.NODESET); - - // 3. Compare in code instead of in XPath - for (int i = 0; i < users.getLength(); i++) { - Element user = (Element) users.item(i); - String u = user.getElementsByTagName("username").item(0).getTextContent(); - String p = user.getElementsByTagName("password").item(0).getTextContent(); - if (username.equals(u) && password.equals(p)) { - return true; - } - } - return false; + XPathExpression compiled = xp.compile("/users/user[@id=$id]"); + xp.setXPathVariableResolver(name -> "id".equals(name.getLocalPart()) ? userIdFromRequest : null); + return (org.w3c.dom.Node) compiled.evaluate(doc, XPathConstants.NODE); } } ``` - In addition, prefer using standard, hardened authentication and data-storage mechanisms - (e.g., databases with parameterized queries, hashed passwords) instead of custom XML-based auth; - if XPath must be used, restrict allowed characters, escape or encode any user-controlled values - before inserting them into expressions, and keep XPath expressions as static as possible. + Key vulnerable patterns covered by this rule include dynamic `XPath.compile/evaluate` calls with tainted expressions. references: - https://owasp.org/www-community/attacks/XPATH_Injection - https://cheatsheetseries.owasp.org/cheatsheets/XML_Security_Cheat_Sheet.html @@ -210,14 +59,17 @@ rules: mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/data-query-injection-sinks.yaml#java-xpath-injection-sinks as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - id: mongodb-injection-in-servlet-app + - id: mongodb-injection severity: ERROR message: >- Potential NoSQL injection: detected untrusted user input going into NoSQL query using the 'where' evaluation operator. @@ -225,216 +77,41 @@ rules: cwe: CWE-943 short-description: MongoDB query injection full-description: |- - MongoDB `$where` injection in Java is a form of NoSQL injection that occurs when - untrusted user input is concatenated into a `$where` clause (server-side JavaScript) - and sent to MongoDB. Because `$where` executes JavaScript on the database server, - an attacker who controls any part of that expression can inject arbitrary JavaScript, - bypass authentication/authorization checks, read or modify data, or cause denial of service. + MongoDB NoSQL injection occurs when untrusted input reaches unsafe `$where` usage, + where query text is interpreted as executable JavaScript. - Vulnerable code sample + Vulnerable example: ```java - import com.mongodb.DB; - import com.mongodb.DBCollection; - import com.mongodb.DBCursor; - import com.mongodb.DBObject; import com.mongodb.BasicDBObject; - import javax.servlet.http.HttpServletRequest; - - public class LoginService { - - private final DB db; - - public LoginService(DB db) { - this.db = db; - } - - public boolean login(HttpServletRequest request) { - String username = request.getParameter("username"); // untrusted input - String password = request.getParameter("password"); // untrusted input - - DBCollection users = db.getCollection("users"); - - // VULNERABLE: user input is concatenated into a $where JavaScript expression - String whereClause = - "this.username == '" + username + "' && this.password == '" + password + "'"; - - DBObject query = new BasicDBObject("$where", whereClause); - - DBCursor cursor = users.find(query); - return cursor.hasNext(); + public class UserQueryService { + public BasicDBObject buildQuery(String whereFromRequest) { + // whereFromRequest is untrusted + return new BasicDBObject("$where", whereFromRequest); // VULNERABLE } } ``` - In this example, an attacker can craft `username` or `password` so that the resulting - `whereClause` becomes malicious JavaScript executed by MongoDB. - - To remediate this issue, avoid using `$where` with string concatenation altogether and always treat - user input as data, not code. Use standard field-based queries and parameterization instead of embedding - user input into JavaScript expressions. Where possible, disable server‑side JavaScript in MongoDB - and apply normal secure coding practices (validation, least privilege, hashed passwords, etc.). - - A safer version of the same logic using field-based queries: + Safe example: ```java - import com.mongodb.client.MongoCollection; - import com.mongodb.client.MongoDatabase; - import com.mongodb.client.model.Filters; - import org.bson.Document; - - import javax.servlet.http.HttpServletRequest; - - public class SafeLoginService { - - private final MongoDatabase db; - - public SafeLoginService(MongoDatabase db) { - this.db = db; - } - - public boolean login(HttpServletRequest request) { - String username = request.getParameter("username"); // still untrusted - String password = request.getParameter("password"); // still untrusted - - MongoCollection users = db.getCollection("users"); - - // SAFE: user input is bound as values, not executed as code - Document user = users.find( - Filters.and( - Filters.eq("username", username), - Filters.eq("password", password) // in reality, compare password hashes - ) - ).first(); - - return user != null; - } - } - ``` - - Key remediation steps: - - Do not use `$where` with dynamic strings built from user input. - - Use field-based queries (`Filters.eq`, `Filters.and`, `new Document("field", value)`, etc.). - - If you must use `$where`, never concatenate raw user input; use strict whitelisting and controlled templates (but prefer avoiding `$where` completely). - - Configure MongoDB to restrict or disable server-side JavaScript where feasible. - references: - - https://owasp.org/www-community/attacks/NoSQL_Injection - - https://www.mongodb.com/docs/manual/core/server-side-javascript/#security-considerations - provenance: https://github.com/semgrep/semgrep-rules/blob/develop/java/mongodb/security/injection/audit/mongodb-nosqli.yaml - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/data-query-injection-sinks.yaml#java-mongodb-nosql-injection - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$INPUT' - - - id: mongodb-injection-in-spring-app - severity: ERROR - message: >- - Potential NoSQL injection: detected untrusted user input going into NoSQL query using the 'where' evaluation operator. - metadata: - cwe: CWE-943 - short-description: MongoDB query injection - full-description: |- - MongoDB `$where` injection in Java is a form of NoSQL injection that occurs when - untrusted user input is concatenated into a `$where` clause (server-side JavaScript) - and sent to MongoDB. Because `$where` executes JavaScript on the database server, - an attacker who controls any part of that expression can inject arbitrary JavaScript, - bypass authentication/authorization checks, read or modify data, or cause denial of service. - - Vulnerable code sample - - ```java - import com.mongodb.DB; - import com.mongodb.DBCollection; - import com.mongodb.DBCursor; - import com.mongodb.DBObject; import com.mongodb.BasicDBObject; - import javax.servlet.http.HttpServletRequest; - - public class LoginService { - - private final DB db; - - public LoginService(DB db) { - this.db = db; - } - - public boolean login(HttpServletRequest request) { - String username = request.getParameter("username"); // untrusted input - String password = request.getParameter("password"); // untrusted input - - DBCollection users = db.getCollection("users"); - - // VULNERABLE: user input is concatenated into a $where JavaScript expression - String whereClause = - "this.username == '" + username + "' && this.password == '" + password + "'"; - - DBObject query = new BasicDBObject("$where", whereClause); - - DBCursor cursor = users.find(query); - return cursor.hasNext(); - } - } - ``` - - In this example, an attacker can craft `username` or `password` so that the resulting - `whereClause` becomes malicious JavaScript executed by MongoDB. - - To remediate this issue, avoid using `$where` with string concatenation altogether and always treat - user input as data, not code. Use standard field-based queries and parameterization instead of embedding - user input into JavaScript expressions. Where possible, disable server‑side JavaScript in MongoDB - and apply normal secure coding practices (validation, least privilege, hashed passwords, etc.). - - A safer version of the same logic using field-based queries: - - ```java - import com.mongodb.client.MongoCollection; - import com.mongodb.client.MongoDatabase; - import com.mongodb.client.model.Filters; - import org.bson.Document; - - import javax.servlet.http.HttpServletRequest; - - public class SafeLoginService { - - private final MongoDatabase db; - - public SafeLoginService(MongoDatabase db) { - this.db = db; - } - - public boolean login(HttpServletRequest request) { - String username = request.getParameter("username"); // still untrusted - String password = request.getParameter("password"); // still untrusted - - MongoCollection users = db.getCollection("users"); - - // SAFE: user input is bound as values, not executed as code - Document user = users.find( - Filters.and( - Filters.eq("username", username), - Filters.eq("password", password) // in reality, compare password hashes - ) - ).first(); + public class UserQueryService { + public BasicDBObject buildQuery(String usernameFromRequest) { + if (usernameFromRequest == null || !usernameFromRequest.matches("[A-Za-z0-9._-]{1,64}")) { + throw new IllegalArgumentException("Invalid username"); + } - return user != null; + // Safe: field-based query, no $where JavaScript + return new BasicDBObject("username", usernameFromRequest); } } ``` - Key remediation steps: - - Do not use `$where` with dynamic strings built from user input. - - Use field-based queries (`Filters.eq`, `Filters.and`, `new Document("field", value)`, etc.). - - If you must use `$where`, never concatenate raw user input; use strict whitelisting and controlled templates (but prefer avoiding `$where` completely). - - Configure MongoDB to restrict or disable server-side JavaScript where feasible. + Key operation family covered by this rule is MongoDB `$where` assignment APIs + (`BasicDBObject.put/append/new ...("$where", ...)`, map-to-query builder flows). references: - https://owasp.org/www-community/attacks/NoSQL_Injection - https://www.mongodb.com/docs/manual/core/server-side-javascript/#security-considerations @@ -444,9 +121,12 @@ rules: mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/data-query-injection-sinks.yaml#java-mongodb-nosql-injection as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$INPUT' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$INPUT' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$INPUT' diff --git a/rules/java/security/external-configuration-control.yaml b/rules/java/security/external-configuration-control.yaml index c1f1dbf..7873aa9 100644 --- a/rules/java/security/external-configuration-control.yaml +++ b/rules/java/security/external-configuration-control.yaml @@ -145,7 +145,7 @@ rules: - pattern: (BeanUtilsBean $B).populate(..., $UNTRUSTED); - pattern: org.apache.commons.beanutils.BeanUtils.populate(..., $UNTRUSTED); - - id: sql-catalog-external-manipulation-in-servlet-app + - id: sql-catalog-external-manipulation severity: ERROR message: >- The application was found using user-supplied input in a `java.sql.Connection`'s @@ -156,255 +156,43 @@ rules: metadata: cwe: CWE-15 short-description: External control of SQL catalog selection - full-description: >- - When untrusted data is passed into `java.sql.Connection.setCatalog(String)`, an attacker can control which database/catalog the application is using. This is similar in effect to letting the user choose the SQL `USE ` statement. In multi-tenant or multi-schema setups, this can break isolation between tenants, bypass authorization logic, and expose or corrupt data held in other catalogs that the database account can access. This is generally categorized as *external control of system or configuration settings* and is closely related to injection-style vulnerabilities. - - Vulnerable code sample - - ```java - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws SQLException, IOException { - - // User-controlled input (e.g., from query string: ?catalog=other_tenant_db) - String catalog = request.getParameter("catalog"); - - Connection conn = dataSource.getConnection(); - - // VULNERABLE: external control of the active database/catalog - conn.setCatalog(catalog); - - try (PreparedStatement ps = conn.prepareStatement( - "SELECT id, email FROM users WHERE id = ?")) { - ps.setInt(1, Integer.parseInt(request.getParameter("id"))); - ResultSet rs = ps.executeQuery(); - // ... - } - } - ``` - - If the underlying database user has access to multiple catalogs (for example, one per tenant), an attacker can supply another tenant's catalog name (or guess other internal catalogs) and read or modify data that should not be available to them. - - To remediate this issue, ensure that the catalog is never directly controlled by untrusted input. Instead: - - 1. **Do not accept arbitrary catalog names from clients.** - Determine the catalog on the server side based on: - - The authenticated user/tenant, and/or - - Static configuration (e.g., per-environment settings), not request parameters. - - 2. **Use a strict whitelist or mapping.** - If you must vary the catalog, map safe, server-side identifiers to catalog names. Never pass raw client-provided strings to `setCatalog`. - - 3. **Enforce least privilege at the database level.** - The DB account used by the application should only have access to the specific catalog(s) it legitimately needs; avoid a single highly privileged account that can access every catalog. - - Safer code example (server-side mapping & validation) - - ```java - // Preconfigured, server-side mapping from tenant ID to allowed catalog - private static final Map TENANT_CATALOGS = Map.of( - "tenantA", "tenant_a_db", - "tenantB", "tenant_b_db" - ); - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws SQLException, IOException { - - String tenantId = getAuthenticatedTenantId(request); // e.g., from session/JWT - - String catalog = TENANT_CATALOGS.get(tenantId); - if (catalog == null) { - // Unknown or unauthorized tenant; do not proceed - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Unauthorized tenant"); - return; - } - - try (Connection conn = dataSource.getConnection()) { - // Safe: catalog comes from trusted server-side configuration - conn.setCatalog(catalog); - - try (PreparedStatement ps = conn.prepareStatement( - "SELECT id, email FROM users WHERE id = ?")) { - ps.setInt(1, Integer.parseInt(request.getParameter("id"))); - ResultSet rs = ps.executeQuery(); - // ... - } - } - } - - private String getAuthenticatedTenantId(HttpServletRequest request) { - // Implementation-specific: derive from authentication/authorization context - return (String) request.getAttribute("tenantId"); - } - ``` - references: - - https://cwe.mitre.org/data/definitions/15.html - - https://owasp.org/www-community/attacks/Injection - provenance: https://gitlab.com/gitlab-org/security-products/sast-rules/-/blob/main/java/unsafe/rule-ExternalConfigControl.yml - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/security/external-configuration-control.yaml#java-sql-catalog-sink - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: sql-catalog-external-manipulation-in-spring-app - severity: ERROR - message: >- - The application was found using user-supplied input in a `java.sql.Connection`'s - `setCatalog` call. This could allow an adversary to supply a different database for the - lifetime of the connection. Allowing external control of system settings can disrupt service - or cause an application to behave in unexpected, and potentially malicious ways. Most likely - this would only cause an error by providing a nonexistent catalog name. - metadata: - cwe: CWE-15 - short-description: External control of SQL catalog selection - full-description: >- - When untrusted data is passed into `java.sql.Connection.setCatalog(String)` in a Spring application - (e.g., from a controller parameter), an attacker can influence which database/catalog the application - uses for subsequent queries. In multi-tenant or multi-schema Spring setups, this may allow a user to - switch to another tenant's catalog, bypass tenant isolation and authorization checks, and read or - modify data belonging to other tenants. This is a form of *external control of system or configuration - settings* and is closely related to injection-style vulnerabilities. + full-description: |- + External configuration control exists when untrusted input reaches configuration APIs such as + `Connection.setCatalog(...)`, allowing runtime database-context switching. - Vulnerable code sample + Vulnerable example: ```java - @RestController - @RequestMapping("/users") - public class UserController { - - private final DataSource dataSource; + import java.sql.Connection; - public UserController(DataSource dataSource) { - this.dataSource = dataSource; - } - - // Example request: GET /users?catalog=other_tenant_db&id=1 - @GetMapping - public List getUsers( - @RequestParam String catalog, - @RequestParam int id) throws SQLException { - - Connection conn = DataSourceUtils.getConnection(dataSource); - try { - // VULNERABLE: user controls the active catalog - conn.setCatalog(catalog); - - try (PreparedStatement ps = conn.prepareStatement( - "SELECT id, email FROM users WHERE id = ?")) { - ps.setInt(1, id); - try (ResultSet rs = ps.executeQuery()) { - List result = new ArrayList<>(); - while (rs.next()) { - User u = new User(); - u.setId(rs.getInt("id")); - u.setEmail(rs.getString("email")); - result.add(u); - } - return result; - } - } - } finally { - DataSourceUtils.releaseConnection(conn, dataSource); - } + public class TenantDbService { + public void selectCatalog(Connection conn, String catalogFromRequest) throws Exception { + // catalogFromRequest is untrusted + conn.setCatalog(catalogFromRequest); // VULNERABLE } } ``` - If the Spring application's database account has access to multiple catalogs, a malicious client - can pass another catalog name via `catalog` and access or modify data in other tenants' databases. - - To remediate this issue, ensure the catalog is determined exclusively by trusted, server-side logic, - not directly from HTTP parameters, headers, or other untrusted input. In Spring applications: - - 1. **Do not bind catalog/schema directly from request parameters.** - The controller should not have a `@RequestParam catalog` (or equivalent) that is passed into `setCatalog`. - - 2. **Derive tenant/catalog from authentication context.** - Use Spring Security to determine the authenticated user/tenant (`Principal`, `@AuthenticationPrincipal`, - or a custom `TenantContext`) and map that to an allowed catalog using server-side configuration. - - 3. **Use a strict whitelist/mapping.** - Maintain a fixed mapping on the server between tenant IDs and catalog names; do not use user-supplied strings as catalog names. - - 4. **Enforce least privilege at the DB level.** - Configure the DataSource to connect with a DB user that only has access to the catalogs required for that tenant/application. - - Safer code example (Spring MVC with tenant → catalog mapping) + Safe example: ```java - @Component - public class TenantCatalogResolver { + import java.sql.Connection; + import java.util.Map; - // Server-side, trusted mapping - private static final Map TENANT_TO_CATALOG = Map.of( - "tenantA", "tenant_a_db", - "tenantB", "tenant_b_db" - ); + public class TenantDbService { + private static final Map ALLOWED = Map.of("tenant-a", "tenant_a_db", "tenant-b", "tenant_b_db"); - public String resolveCatalogForTenant(String tenantId) { - String catalog = TENANT_TO_CATALOG.get(tenantId); + public void selectCatalog(Connection conn, String tenantIdFromAuth) throws Exception { + String catalog = ALLOWED.get(tenantIdFromAuth); if (catalog == null) { - throw new IllegalArgumentException("Unknown tenant: " + tenantId); - } - return catalog; - } - } - - @RestController - @RequestMapping("/users") - public class SafeUserController { - - private final DataSource dataSource; - private final TenantCatalogResolver tenantCatalogResolver; - - public SafeUserController(DataSource dataSource, - TenantCatalogResolver tenantCatalogResolver) { - this.dataSource = dataSource; - this.tenantCatalogResolver = tenantCatalogResolver; - } - - @GetMapping - public List getUsers( - @RequestParam int id, - @AuthenticationPrincipal CustomUserDetails userDetails) throws SQLException { - - String tenantId = userDetails.getTenantId(); // from Spring Security - String catalog = tenantCatalogResolver.resolveCatalogForTenant(tenantId); - - Connection conn = DataSourceUtils.getConnection(dataSource); - try { - // Safe: catalog comes from trusted, server-side mapping - conn.setCatalog(catalog); - - try (PreparedStatement ps = conn.prepareStatement( - "SELECT id, email FROM users WHERE id = ?")) { - ps.setInt(1, id); - try (ResultSet rs = ps.executeQuery()) { - List result = new ArrayList<>(); - while (rs.next()) { - User u = new User(); - u.setId(rs.getInt("id")); - u.setEmail(rs.getString("email")); - result.add(u); - } - return result; - } - } - } finally { - DataSourceUtils.releaseConnection(conn, dataSource); + throw new SecurityException("Unknown tenant"); } + conn.setCatalog(catalog); } } ``` - In more advanced Spring setups, you can also implement multi-tenancy by using `AbstractRoutingDataSource` - or a similar mechanism, where routing is based on a trusted `TenantContext` and **not** on raw HTTP input. + Key operation family covered by this rule is catalog switching via JDBC `Connection.setCatalog(...)`. references: - https://cwe.mitre.org/data/definitions/15.html - https://owasp.org/www-community/attacks/Injection @@ -414,12 +202,15 @@ rules: mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/security/external-configuration-control.yaml#java-sql-catalog-sink as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - id: java-sql-catalog-sink options: @@ -433,7 +224,7 @@ rules: patterns: - pattern: (java.sql.Connection $CONN).setCatalog($UNTRUSTED); - - id: unsafe-reflection-in-servlet-app + - id: unsafe-reflection severity: WARNING message: >- If an attacker can supply values that the application then uses to determine which @@ -447,198 +238,44 @@ rules: - CWE-470 short-description: Allowing user input to control which classes or methods are used can let attackers manipulate program flow full-description: |- - Unsafe reflection in Java occurs when user-controlled input is used to determine which classes or methods - are loaded or invoked via the reflection API (e.g., `Class.forName`, `Method.invoke`). - If an attacker can influence the class name passed to `Class.forName`, they may be able to load arbitrary - application or library classes, trigger dangerous static initializers or constructors, - or chain into further vulnerabilities, leading to code execution, privilege escalation, or denial of service. + Unsafe reflection occurs when untrusted input reaches reflection target-selection operations, + such as class loading with `Class.forName(...)`. - Vulnerable code sample + Vulnerable example: ```java - import javax.servlet.*; - import javax.servlet.http.*; - import java.io.IOException; - - public class DynamicLoaderServlet extends HttpServlet { - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - // User-controlled data from request parameter - String className = request.getParameter("className"); - - try { - // UNSAFE: user input directly controls Class.forName - Class clazz = Class.forName(className); - Object instance = clazz.getDeclaredConstructor().newInstance(); - - // Do something with the instance... - response.getWriter().println("Loaded class: " + clazz.getName()); - } catch (Exception e) { - throw new ServletException(e); - } + public class ReflectionService { + public Object create(String classNameFromRequest) throws Exception { + // classNameFromRequest is untrusted + Class c = Class.forName(classNameFromRequest); // VULNERABLE + return c.getDeclaredConstructor().newInstance(); } } ``` - To remediate this issue, never pass raw user input directly into `Class.forName` (or other reflection APIs). Instead: - - - Use an allowlist (fixed set) of permitted classes or a mapping from user input to known-safe classes. - - Validate input strictly (e.g., only allow known identifiers, not full class names from the client). - - Prefer explicit logic (e.g., `if/else`, `switch`, enums, or a configuration map) over arbitrary reflective loading. - - Safe example using a whitelist mapping: + Safe example: ```java - import javax.servlet.*; - import javax.servlet.http.*; - import java.io.IOException; - import java.util.HashMap; import java.util.Map; - public class SafeDynamicLoaderServlet extends HttpServlet { - - // Allowlist of logical names to actual classes - private static final Map> ALLOWED_CLASSES = new HashMap<>(); - static { - ALLOWED_CLASSES.put("basicReport", com.example.reports.BasicReport.class); - ALLOWED_CLASSES.put("summaryReport", com.example.reports.SummaryReport.class); - // Add more allowed classes here - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String type = request.getParameter("reportType"); - - Class clazz = ALLOWED_CLASSES.get(type); - if (clazz == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid report type"); - return; - } - - try { - Object instance = clazz.getDeclaredConstructor().newInstance(); - // Safe: only instances of known, vetted classes are created - response.getWriter().println("Generated report of type: " + type); - } catch (Exception e) { - throw new ServletException(e); - } - } - } - ``` - references: - - https://cwe.mitre.org/data/definitions/470.html - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/unsafe-reflection.yaml#java-unsafe-reflection-sinks - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: unsafe-reflection-in-spring-app - severity: WARNING - message: >- - If an attacker can supply values that the application then uses to determine which - class to instantiate or which method to invoke, - the potential exists for the attacker to create control flow paths through the application - that were not intended by the application developers. - This attack vector may allow the attacker to bypass authentication or access control - checks or otherwise cause the application to behave in an unexpected manner. - metadata: - cwe: - - CWE-470 - short-description: Allowing user input to control which classes or methods are used can let attackers manipulate program flow - full-description: |- - Unsafe reflection in Java occurs when user-controlled input is used to determine which classes or methods - are loaded or invoked via the reflection API (e.g., `Class.forName`, `Method.invoke`). - If an attacker can influence the class name passed to `Class.forName`, they may be able to load arbitrary - application or library classes, trigger dangerous static initializers or constructors, - or chain into further vulnerabilities, leading to code execution, privilege escalation, or denial of service. - - Vulnerable code sample + public class ReflectionService { + private static final Map> ALLOWED = Map.of( + "basic", com.example.SafeBasic.class, + "advanced", com.example.SafeAdvanced.class + ); - ```java - import javax.servlet.*; - import javax.servlet.http.*; - import java.io.IOException; - - public class DynamicLoaderServlet extends HttpServlet { - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - // User-controlled data from request parameter - String className = request.getParameter("className"); - - try { - // UNSAFE: user input directly controls Class.forName - Class clazz = Class.forName(className); - Object instance = clazz.getDeclaredConstructor().newInstance(); - - // Do something with the instance... - response.getWriter().println("Loaded class: " + clazz.getName()); - } catch (Exception e) { - throw new ServletException(e); + public Object create(String typeFromRequest) throws Exception { + Class c = ALLOWED.get(typeFromRequest); + if (c == null) { + throw new IllegalArgumentException("Unsupported type"); } + return c.getDeclaredConstructor().newInstance(); } } ``` - To remediate this issue, never pass raw user input directly into `Class.forName` (or other reflection APIs). Instead: - - - Use an allowlist (fixed set) of permitted classes or a mapping from user input to known-safe classes. - - Validate input strictly (e.g., only allow known identifiers, not full class names from the client). - - Prefer explicit logic (e.g., `if/else`, `switch`, enums, or a configuration map) over arbitrary reflective loading. - - Safe example using a whitelist mapping: - - ```java - import javax.servlet.*; - import javax.servlet.http.*; - import java.io.IOException; - import java.util.HashMap; - import java.util.Map; - - public class SafeDynamicLoaderServlet extends HttpServlet { - - // Allowlist of logical names to actual classes - private static final Map> ALLOWED_CLASSES = new HashMap<>(); - static { - ALLOWED_CLASSES.put("basicReport", com.example.reports.BasicReport.class); - ALLOWED_CLASSES.put("summaryReport", com.example.reports.SummaryReport.class); - // Add more allowed classes here - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String type = request.getParameter("reportType"); - - Class clazz = ALLOWED_CLASSES.get(type); - if (clazz == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid report type"); - return; - } - - try { - Object instance = clazz.getDeclaredConstructor().newInstance(); - // Safe: only instances of known, vetted classes are created - response.getWriter().println("Generated report of type: " + type); - } catch (Exception e) { - throw new ServletException(e); - } - } - } - ``` + Key vulnerable patterns covered by this rule include `Class.forName(...)` and related reflection target resolution + with tainted class/member identifiers. references: - https://cwe.mitre.org/data/definitions/470.html languages: @@ -646,9 +283,12 @@ rules: mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/unsafe-reflection.yaml#java-unsafe-reflection-sinks as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' diff --git a/rules/java/security/ldap.yaml b/rules/java/security/ldap.yaml index 9229f9c..cf161e7 100644 --- a/rules/java/security/ldap.yaml +++ b/rules/java/security/ldap.yaml @@ -1,5 +1,5 @@ rules: - - id: ldap-injection-in-servlet-app + - id: ldap-injection severity: ERROR message: >- Potential LDAP injection: detected untrusted user input going into a LDAP query @@ -7,214 +7,44 @@ rules: cwe: CWE-90 short-description: Potential LDAP Injection full-description: |- - LDAP injection is a security vulnerability that occurs when untrusted input is used to dynamically - construct LDAP queries (filters or distinguished names) without proper validation or encoding. - In Java, this often appears in code that uses JNDI (or LDAP libraries) and concatenates user-controlled - data into LDAP search filters. An attacker can manipulate the structure of the LDAP query to bypass authentication, - escalate privileges, or retrieve unauthorized data. + LDAP injection occurs when untrusted input reaches LDAP query APIs as filter/query text. + This rule focuses on LDAP lookup/search calls in JNDI, UnboundID, and Spring LDAP operations. - ```java - // Vulnerable code sample (Java + JNDI) - - import javax.naming.Context; - import javax.naming.NamingEnumeration; - import javax.naming.directory.*; - - public class LdapAuthService { - - private final DirContext ctx; - private final String baseDn; - - public LdapAuthService(DirContext ctx, String baseDn) { - this.ctx = ctx; - this.baseDn = baseDn; - } - - public boolean authenticate(String username, String password) throws Exception { - // User-controlled input is concatenated directly into the LDAP filter - String filter = "(&(uid=" + username + ")(userPassword=" + password + "))"; - - SearchControls controls = new SearchControls(); - controls.setSearchScope(SearchControls.SUBTREE_SCOPE); - - NamingEnumeration results = - ctx.search(baseDn, filter, controls); - - // If any entry matches, authentication is considered successful - return results.hasMore(); - } - } - ``` - - In this example, if an attacker supplies a crafted `username` such as `*)(|(uid=*))`, - the resulting filter changes its logic and may cause the query to match unintended entries, - potentially bypassing authentication or exposing data. - - To remediate this issue, avoid building LDAP filters or DNs with raw string concatenation of user input. - Instead, use parameterized LDAP queries / filter argument substitution (which properly encodes special characters), - and perform strict input validation. For example: + Vulnerable example: ```java - // Safe code sample (Java + JNDI with filter arguments) - - import javax.naming.Context; - import javax.naming.NamingEnumeration; - import javax.naming.directory.*; + import javax.naming.directory.DirContext; + import javax.naming.directory.SearchControls; - public class SafeLdapAuthService { - - private final DirContext ctx; - private final String baseDn; - - public SafeLdapAuthService(DirContext ctx, String baseDn) { - this.ctx = ctx; - this.baseDn = baseDn; - } - - public boolean authenticate(String username, String password) throws Exception { - // Example of basic input validation (application-specific) - if (!username.matches("[a-zA-Z0-9._-]{1,32}")) { - return false; // reject unexpected characters / lengths - } - - String filter = "(&(uid={0})(userPassword={1}))"; - - SearchControls controls = new SearchControls(); - controls.setSearchScope(SearchControls.SUBTREE_SCOPE); - - // JNDI will safely encode filter arguments to prevent injection - Object[] filterArgs = new Object[] { username, password }; - - NamingEnumeration results = - ctx.search(baseDn, filter, filterArgs, controls); - - return results.hasMore(); + public class LdapService { + public void find(DirContext ctx, String filterFromRequest) throws Exception { + // filterFromRequest is untrusted + ctx.search("ou=users,dc=example,dc=com", filterFromRequest, new SearchControls()); // VULNERABLE } } ``` - Key remediation steps: - 1. **Never concatenate untrusted input into LDAP filters or DNs.** - 2. **Use parameterized LDAP APIs / filter arguments** where available (as shown with `{0}`, `{1}` and `filterArgs`). - 3. **Validate and sanitize inputs** with strict allowlists (e.g., allowed characters, length). - 4. **Use least-privilege LDAP accounts** so that even if a query is abused, impact is limited. - 5. **Centralize LDAP access** through a well-reviewed data access layer or library that handles encoding correctly. - references: - - https://owasp.org/www-community/attacks/LDAP_Injection - - https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html - provenance: https://gitlab.com/gitlab-org/security-products/sast-rules/-/blob/main/java/inject/rule-LDAPInjection.yml - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/ldap-injection-sinks.yaml#java-ldap-injection-sinks - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$QUERY' - - - id: ldap-injection-in-spring-app - severity: ERROR - message: >- - Potential LDAP injection: detected untrusted user input going into a LDAP query - metadata: - cwe: CWE-90 - short-description: Potential LDAP Injection - full-description: |- - LDAP injection is a security vulnerability that occurs when untrusted input is used to dynamically - construct LDAP queries (filters or distinguished names) without proper validation or encoding. - In Java, this often appears in code that uses JNDI (or LDAP libraries) and concatenates user-controlled - data into LDAP search filters. An attacker can manipulate the structure of the LDAP query to bypass authentication, - escalate privileges, or retrieve unauthorized data. - - ```java - // Vulnerable code sample (Java + JNDI) - - import javax.naming.Context; - import javax.naming.NamingEnumeration; - import javax.naming.directory.*; - - public class LdapAuthService { - - private final DirContext ctx; - private final String baseDn; - - public LdapAuthService(DirContext ctx, String baseDn) { - this.ctx = ctx; - this.baseDn = baseDn; - } - - public boolean authenticate(String username, String password) throws Exception { - // User-controlled input is concatenated directly into the LDAP filter - String filter = "(&(uid=" + username + ")(userPassword=" + password + "))"; - - SearchControls controls = new SearchControls(); - controls.setSearchScope(SearchControls.SUBTREE_SCOPE); - - NamingEnumeration results = - ctx.search(baseDn, filter, controls); - - // If any entry matches, authentication is considered successful - return results.hasMore(); - } - } - ``` - - In this example, if an attacker supplies a crafted `username` such as `*)(|(uid=*))`, - the resulting filter changes its logic and may cause the query to match unintended entries, - potentially bypassing authentication or exposing data. - - To remediate this issue, avoid building LDAP filters or DNs with raw string concatenation of user input. - Instead, use parameterized LDAP queries / filter argument substitution (which properly encodes special characters), - and perform strict input validation. For example: + Safe example: ```java - // Safe code sample (Java + JNDI with filter arguments) + import javax.naming.directory.DirContext; + import javax.naming.directory.SearchControls; - import javax.naming.Context; - import javax.naming.NamingEnumeration; - import javax.naming.directory.*; - - public class SafeLdapAuthService { - - private final DirContext ctx; - private final String baseDn; - - public SafeLdapAuthService(DirContext ctx, String baseDn) { - this.ctx = ctx; - this.baseDn = baseDn; - } - - public boolean authenticate(String username, String password) throws Exception { - // Example of basic input validation (application-specific) - if (!username.matches("[a-zA-Z0-9._-]{1,32}")) { - return false; // reject unexpected characters / lengths + public class LdapService { + public void findByUid(DirContext ctx, String uidFromRequest) throws Exception { + if (uidFromRequest == null || !uidFromRequest.matches("[A-Za-z0-9._-]{1,64}")) { + throw new IllegalArgumentException("Invalid uid"); } - String filter = "(&(uid={0})(userPassword={1}))"; - - SearchControls controls = new SearchControls(); - controls.setSearchScope(SearchControls.SUBTREE_SCOPE); - - // JNDI will safely encode filter arguments to prevent injection - Object[] filterArgs = new Object[] { username, password }; - - NamingEnumeration results = - ctx.search(baseDn, filter, filterArgs, controls); - - return results.hasMore(); + String filter = "(uid={0})"; + Object[] args = new Object[]{uidFromRequest}; + ctx.search("ou=users,dc=example,dc=com", filter, args, new SearchControls()); } } ``` - Key remediation steps: - 1. **Never concatenate untrusted input into LDAP filters or DNs.** - 2. **Use parameterized LDAP APIs / filter arguments** where available (as shown with `{0}`, `{1}` and `filterArgs`). - 3. **Validate and sanitize inputs** with strict allowlists (e.g., allowed characters, length). - 4. **Use least-privilege LDAP accounts** so that even if a query is abused, impact is limited. - 5. **Centralize LDAP access** through a well-reviewed data access layer or library that handles encoding correctly. + Key vulnerable patterns covered by this rule include `DirContext/LdapContext.search`, `lookup`, + UnboundID `LDAPConnection.search`, and Spring LDAP search/lookup APIs with tainted query data. references: - https://owasp.org/www-community/attacks/LDAP_Injection - https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html @@ -224,12 +54,15 @@ rules: mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/ldap-injection-sinks.yaml#java-ldap-injection-sinks as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$QUERY' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$QUERY' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$QUERY' - id: ldap-entry-poisoning severity: WARNING diff --git a/rules/java/security/log-injection.yaml b/rules/java/security/log-injection.yaml index 94999a9..88fddc0 100644 --- a/rules/java/security/log-injection.yaml +++ b/rules/java/security/log-injection.yaml @@ -1,5 +1,5 @@ rules: - - id: log-injection-in-servlet-app + - id: log-injection severity: NOTE message: >- When data from an untrusted source is put into a logger and not neutralized correctly, @@ -8,196 +8,43 @@ rules: cwe: CWE-117 short-description: Logging an untrusted data might cause unwanted log entries forging full-description: |- - Logging untrusted user input directly into application logs can lead to **log injection / log forging** vulnerabilities. - An attacker can include control characters (such as `\r` and `\n`) or log-format tokens in their input so that, when it is logged, - it creates fake log entries, hides or alters real events, or breaks log parsers and SIEM rules. In some cases, untrusted data - in log messages can also trigger dangerous features of logging frameworks (e.g., message lookups), - potentially escalating to more severe issues such as remote code execution if the framework is misconfigured or unpatched. - Additionally, logging unfiltered user input increases the risk of leaking sensitive or personal data into logs. + Log injection occurs when untrusted input is passed to logging APIs without normalization. + This rule focuses on logger calls (e.g., `logger.info(...)`, `logger.warn(...)`, `logger.error(...)`). - ```java - // Vulnerable code sample - import java.io.IOException; - import javax.servlet.ServletException; - import javax.servlet.http.*; - import org.apache.logging.log4j.LogManager; - import org.apache.logging.log4j.Logger; - - public class LoginServlet extends HttpServlet { - - private static final Logger logger = LogManager.getLogger(LoginServlet.class); - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String username = request.getParameter("username"); // Untrusted user input - - // VULNERABLE: user input is concatenated directly into the log message - logger.warn("Failed login attempt for user: " + username); - - // ... - } - } - ``` - - To remediate this issue, validate and sanitize all untrusted data before logging, - avoid using user input as the log message or pattern itself, and use parameterized - logging APIs so the framework treats the data as values, not as formatting or lookup expressions. - Also ensure your logging framework is up to date and dangerous features - (such as JNDI lookups in older Log4j2 versions) are disabled. - - A safer approach might look like this: + Vulnerable example: ```java - import java.io.IOException; - import javax.servlet.ServletException; - import javax.servlet.http.*; - import org.apache.logging.log4j.LogManager; - import org.apache.logging.log4j.Logger; - - public class LoginServlet extends HttpServlet { - - private static final Logger logger = LogManager.getLogger(LoginServlet.class); - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String username = request.getParameter("username"); - - // Sanitize untrusted input for logging: remove CR/LF and other control chars - String safeUsername = sanitizeForLog(username); - - // SAFE: use parameterized logging; user data is treated purely as data - logger.warn("Failed login attempt for user [{}]", safeUsername); - - // ... - } - - private String sanitizeForLog(String value) { - if (value == null) { - return ""; - } - // Replace CR, LF, and TAB with a safe placeholder; extend as needed - return value.replaceAll("[\\r\\n\\t]", "_"); - } - } - ``` - - Additional best practices include: not logging secrets (passwords, tokens, full credit card numbers), - standardizing a log format, and reviewing log sinks/consumers (parsers, SIEMs) to ensure they are robust against malformed records. - refernces: - - https://owasp.org/www-community/attacks/Log_Injection - - https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html - provenance: https://semgrep.dev/r/gitlab.find_sec_bugs.CRLF_INJECTION_LOGS-1 - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/logging-sinks.yaml#java-logging-sinks - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$DATA' - - - id: log-injection-in-spring-app - severity: NOTE - message: >- - When data from an untrusted source is put into a logger and not neutralized correctly, - an attacker could forge log entries or include malicious content. - metadata: - cwe: CWE-117 - short-description: Logging an untrusted data might cause unwanted log entries forging - full-description: |- - Logging untrusted user input directly in Spring-based Java web applications (e.g., Spring MVC / Spring Boot) - can lead to **log injection / log forging** and **information disclosure**. - Attackers can submit data containing control characters (`\r`, `\n`) to inject fake log entries or - hide real ones, or supply strings that interact badly with log parsers / SIEM rules. - If vulnerable logging libraries or patterns are used (e.g., unsafe Log4j2 configurations), - crafted values can even trigger dangerous features such as message lookups. - Logging unfiltered request data in Spring controllers also increases the risk of - accidentally storing credentials, tokens, and other sensitive data in logs. - - ```java - // Vulnerable Spring Boot controller example - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.http.ResponseEntity; - import org.springframework.web.bind.annotation.*; - - @RestController - @RequestMapping("/login") - public class LoginController { - private static final Logger logger = LoggerFactory.getLogger(LoginController.class); + public class AuditService { + private static final Logger LOG = LoggerFactory.getLogger(AuditService.class); - @PostMapping - public ResponseEntity login( - @RequestParam String username, // untrusted - @RequestParam String password) { // untrusted & sensitive - - // VULNERABLE: - // - Direct string concatenation with untrusted input - // - Logs sensitive data (password) - // - Allows CR/LF and other control characters in logs - logger.warn("Failed login for user: " + username + " with password: " + password); - - return ResponseEntity.status(401).body("Login failed"); + public void logLoginFailure(String usernameFromRequest) { + // usernameFromRequest is untrusted + LOG.warn("Failed login for user=" + usernameFromRequest); // VULNERABLE } } ``` - To remediate this issue, treat all Spring MVC / WebFlux request data as untrusted, sanitize before logging, - avoid logging secrets, and use parameterized logging. Ensure your logging framework (Logback, Log4j2, etc.) - is up to date and configured securely (e.g., no unsafe message lookups in Log4j2). A safer Spring example: + Safe example: ```java import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.http.*; - import org.springframework.web.bind.annotation.*; - - @RestController - @RequestMapping("/login") - public class LoginController { - - private static final Logger logger = LoggerFactory.getLogger(LoginController.class); - @PostMapping - public ResponseEntity login(@RequestParam String username, - @RequestParam String password) { + public class AuditService { + private static final Logger LOG = LoggerFactory.getLogger(AuditService.class); - // Do NOT log passwords or other secrets - String safeUsername = sanitizeForLog(username); - - // SAFE-ER: parameterized logging + sanitized data - logger.warn("Failed login attempt for user [{}]", safeUsername); - - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Login failed"); - } - - private String sanitizeForLog(String value) { - if (value == null) { - return ""; - } - // Remove CR, LF, TAB and other control characters; extend as needed - return value.replaceAll("[\\r\\n\\t\\x00-\\x1F]", "_"); + public void logLoginFailure(String usernameFromRequest) { + String safe = usernameFromRequest == null ? "" : usernameFromRequest.replaceAll("[\\r\\n\\t]", "_"); + LOG.warn("Failed login for user={}", safe); } } ``` - Additional Spring-focused hardening steps: - - - Configure centralized logging in `application.yml` / `application.properties` to avoid logging raw request - bodies and headers by default (e.g., be cautious with `logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG`). - - Implement a `OncePerRequestFilter` or `HandlerInterceptor` to normalize or strip dangerous - characters from values that are logged across the application. - - Ensure dependencies (Logback / Log4j2 / SLF4J) are on supported, patched versions; - for Log4j2, avoid deprecated message lookup features and unsafe patterns. + Key vulnerable patterns covered by this rule include common Java logging APIs (`slf4j`, `log4j`, + `java.util.logging`, `commons-logging`, `tinylog`) when tainted data is logged. refernces: - https://owasp.org/www-community/attacks/Log_Injection - https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html @@ -207,12 +54,15 @@ rules: mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/logging-sinks.yaml#java-logging-sinks as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$DATA' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$DATA' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$DATA' - id: seam-log-injection severity: ERROR diff --git a/rules/java/security/path-traversal.yaml b/rules/java/security/path-traversal.yaml index aaa2d81..5d11efd 100644 --- a/rules/java/security/path-traversal.yaml +++ b/rules/java/security/path-traversal.yaml @@ -1,5 +1,5 @@ rules: - - id: path-traversal-in-servlet-app + - id: path-traversal severity: ERROR message: >- Potential path traversal: detected user input controlling a file path. An attacker could control the location of this @@ -9,118 +9,55 @@ rules: cwe: CWE-22 short-description: Interaction with file system via untrusted path, potential path traversal full-description: |- - Path traversal (also known as directory traversal) is a vulnerability that occurs when user-controlled input is used to construct filesystem paths without proper validation. An attacker can manipulate the path (for example, using sequences like `../`) to break out of intended directories and access or modify files elsewhere on the server, such as configuration files, source code, or sensitive data. + Path traversal risk exists when untrusted data reaches filesystem APIs (file reads/writes, + file metadata checks, resource loaders, or path-based utility calls). - **Vulnerable code sample** + Vulnerable example: ```java - import javax.servlet.ServletException; - import javax.servlet.http.*; - import java.io.*; + import java.nio.file.Files; + import java.nio.file.Path; + import java.nio.file.Paths; - public class DownloadServlet extends HttpServlet { + public class FileService { + public byte[] read(String pathFromRequest) throws Exception { + // pathFromRequest is untrusted + Path p = Paths.get(pathFromRequest); - private static final String BASE_DIR = "/var/www/uploads/"; - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - // User provides file name via query parameter ?file=... - String fileName = request.getParameter("file"); - - // VULNERABLE: directly concatenating user input into a file path - File file = new File(BASE_DIR + fileName); - - if (!file.exists()) { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - try (FileInputStream fis = new FileInputStream(file); - OutputStream out = response.getOutputStream()) { - - response.setContentType("application/octet-stream"); - response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\""); - - byte[] buffer = new byte[4096]; - int read; - while ((read = fis.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - } + // VULNERABLE + return Files.readAllBytes(p); } } ``` - An attacker could supply a value like `../../../../etc/passwd` as `file`, potentially reading sensitive files outside `/var/www/uploads/`. - - To remediate this issue, validate and constrain all file path input and ensure that the resolved path stays within an expected directory. Prefer allowlists (known-safe file names or IDs) and use canonical/normalized paths for checks. - - **Safer approach using `java.nio.file.Path` and canonical checks:** + Safe example: ```java - import javax.servlet.ServletException; - import javax.servlet.http.*; - import java.io.*; - import java.nio.file.*; + import java.nio.file.Files; + import java.nio.file.Path; + import java.nio.file.Paths; - public class SafeDownloadServlet extends HttpServlet { + public class FileService { + private static final Path BASE = Paths.get("/srv/app/files").toAbsolutePath().normalize(); - private static final Path BASE_DIR = Paths.get("/var/www/uploads").toAbsolutePath().normalize(); - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String fileName = request.getParameter("file"); - if (fileName == null || fileName.isEmpty()) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing file parameter"); - return; - } - - // Optional: allowlist or pattern validation - // e.g., only letters, numbers, dot, dash, underscore - if (!fileName.matches("[A-Za-z0-9._-]+")) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid file name"); - return; - } - - // Resolve and normalize the target path - Path target = BASE_DIR.resolve(fileName).normalize(); - - // Ensure the final path is still under the base directory - if (!target.startsWith(BASE_DIR)) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied"); - return; + public byte[] read(String pathFromRequest) throws Exception { + if (pathFromRequest == null || !pathFromRequest.matches("[A-Za-z0-9._-]+")) { + throw new IllegalArgumentException("Invalid file name"); } - if (!Files.exists(target) || !Files.isRegularFile(target)) { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return; + Path target = BASE.resolve(pathFromRequest).normalize(); + if (!target.startsWith(BASE)) { + throw new SecurityException("Traversal attempt"); } - response.setContentType("application/octet-stream"); - response.setHeader("Content-Disposition", "attachment; filename=\"" + target.getFileName().toString() + "\""); - - try (InputStream in = Files.newInputStream(target); - OutputStream out = response.getOutputStream()) { - - byte[] buffer = new byte[4096]; - int read; - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - } + // Safe usage: canonicalized, constrained path + return Files.readAllBytes(target); } } ``` - Key remediation steps: - - Treat all path components from users as untrusted. - - Use allowlists for file names or map user inputs to internal IDs instead of raw paths where possible. - - Normalize/canonicalize paths (`normalize()`, `toRealPath()`) and verify they stay within an intended base directory. - - Avoid echoing raw paths or detailed errors back to the client. + Key vulnerable patterns covered by this rule include `java.io.File*`, `java.nio.file.Files.*`, + Spring resource/file utilities, and classpath/resource lookups with tainted path values. references: - https://owasp.org/www-community/attacks/Path_Traversal - https://en.wikipedia.org/wiki/Directory_traversal @@ -131,143 +68,11 @@ rules: join: refs: - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/path-traversal-sinks.yaml#java-path-traversal-sinks - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$FILE' - - - id: path-traversal-in-spring-app - severity: ERROR - message: >- - Potential path traversal: detected user input controlling a file path. An attacker could control the location of this - file, to include going backwards in the directory with '../'. To address this, ensure that user-controlled - variables in file paths are sanitized. - metadata: - cwe: CWE-22 - short-description: Interaction with file system via untrusted path, potential path traversal - full-description: |- - Path traversal (also known as directory traversal) in Spring-based Java applications occurs when user-controlled input is used to construct filesystem paths in controllers, services, or static resource handlers without proper validation. Attackers can exploit this by sending values containing `../` or absolute paths to escape the intended directory and access sensitive files (e.g., `/etc/passwd`, configuration files, source code) on the server. - - **Vulnerable code sample** - - ```java - import org.springframework.core.io.ByteArrayResource; - import org.springframework.http.*; - import org.springframework.web.bind.annotation.*; - - import java.io.IOException; - import java.nio.file.*; - - @RestController - @RequestMapping("/files") - public class FileDownloadController { - - private static final String BASE_DIR = "/var/app/uploads/"; - - @GetMapping("/{*fileName}") - public ResponseEntity download(@PathVariable String fileName) throws IOException { - - // VULNERABLE: directly concatenating user input into a file path - Path path = Paths.get(BASE_DIR + fileName); - - if (!Files.exists(path)) { - return ResponseEntity.notFound().build(); - } - - byte[] data = Files.readAllBytes(path); - ByteArrayResource resource = new ByteArrayResource(data); - - return ResponseEntity.ok() - .contentType(MediaType.APPLICATION_OCTET_STREAM) - .header(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=\"" + path.getFileName().toString() + "\"") - .body(resource); - } - } - ``` - - An attacker could call `/files/../../../../etc/passwd` or URL-encoded variants and potentially retrieve sensitive system files instead of only files under `/var/app/uploads/`. - - To remediate this issue, validate and constrain all file-related input in Spring controllers and services, normalize the path, and ensure the final resolved path stays within an allowed base directory. Prefer allowlists (permitted filenames/IDs) instead of accepting arbitrary paths. - - **Safer Spring controller example using `Path` normalization and checks:** - - ```java - import org.springframework.core.io.Resource; - import org.springframework.core.io.UrlResource; - import org.springframework.http.*; - import org.springframework.web.bind.annotation.*; - import org.springframework.web.server.ResponseStatusException; - - import java.io.IOException; - import java.net.MalformedURLException; - import java.nio.file.*; - - @RestController - @RequestMapping("/files") - public class SafeFileDownloadController { - - private static final Path BASE_DIR = - Paths.get("/var/app/uploads").toAbsolutePath().normalize(); - - @GetMapping("/{*fileName}") - public ResponseEntity download(@PathVariable String fileName) { - - // 1. Basic allowlist / pattern validation - if (fileName == null || !fileName.matches("[A-Za-z0-9._-]+")) { - throw new ResponseStatusException( - HttpStatus.BAD_REQUEST, "Invalid file name"); - } - - // 2. Resolve and normalize against a fixed base directory - Path target = BASE_DIR.resolve(fileName).normalize(); - - // 3. Enforce that the resolved path is still under BASE_DIR - if (!target.startsWith(BASE_DIR)) { - throw new ResponseStatusException( - HttpStatus.FORBIDDEN, "Access denied"); - } - - if (!Files.exists(target) || !Files.isRegularFile(target)) { - throw new ResponseStatusException( - HttpStatus.NOT_FOUND, "File not found"); - } - - Resource resource; - try { - resource = new UrlResource(target.toUri()); - } catch (MalformedURLException e) { - throw new ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, "Could not read file", e); - } - - return ResponseEntity.ok() - .contentType(MediaType.APPLICATION_OCTET_STREAM) - .header(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=\"" + target.getFileName().toString() + "\"") - .body(resource); - } - } - ``` - - Additional Spring-specific recommendations: - - - For static resources (e.g., via `WebMvcConfigurer#addResourceHandlers`), use `PathResourceResolver` and configure `setAllowedLocations(...)` so that only predefined directories can be served. - - Prefer storing and serving files from controlled locations (e.g., application storage directory or classpath) rather than arbitrary filesystem paths derived from user input. - - Consider mapping user-visible IDs to filenames on the server rather than exposing filenames directly. - references: - - https://owasp.org/www-community/attacks/Path_Traversal - - https://en.wikipedia.org/wiki/Directory_traversal - provenance: https://github.com/semgrep/semgrep-rules/blob/develop/java/spring/security/injection/tainted-file-path.yaml - languages: - - java - mode: join - join: - refs: + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-path-source.yaml#spring-untrusted-path-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/path-traversal-sinks.yaml#java-path-traversal-sinks as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$FILE' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$FILE' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$FILE' diff --git a/rules/java/security/sqli.yaml b/rules/java/security/sqli.yaml index 9c0d0e8..3b1c5a7 100644 --- a/rules/java/security/sqli.yaml +++ b/rules/java/security/sqli.yaml @@ -1,83 +1,55 @@ rules: - - id: sql-injection-in-servlet-app + - id: sql-injection severity: ERROR message: >- - Potential SQL injection: detected unescaped input from a user-manipulated data source going into a SQL sink or statement + Potential SQL injection: detected unescaped input from a user-manipulated data source going into a SQL execution call or statement metadata: cwe: CWE-89 short-description: Potential SQL injection full-description: |- - SQL injection is a vulnerability where untrusted input is concatenated into SQL queries and sent to the database without - proper validation or parameterization. In Java servlet applications, this typically happens when request parameters - are directly embedded into SQL strings. An attacker can craft input that changes the structure of the SQL query, - allowing unauthorized data access, data modification, or even full compromise of the database. + SQL injection happens when untrusted data is embedded into a SQL string that is executed by a SQL execution API. + This rule focuses on the security-sensitive call itself (for example `Statement.executeQuery(...)`, `JdbcTemplate.query(...)`, + or `EntityManager.createNativeQuery(...)`). - Vulnerable code sample + Vulnerable example: ```java - public class UserProfileServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String userId = request.getParameter("userId"); // untrusted input - - try (Connection conn = dataSource.getConnection(); - Statement stmt = conn.createStatement()) { - - // Vulnerable: userId is concatenated directly into the SQL string - String sql = "SELECT * FROM users WHERE id = '" + userId + "'"; - ResultSet rs = stmt.executeQuery(sql); - - // ... process result set ... - - } catch (SQLException e) { - throw new ServletException(e); - } + import java.sql.Connection; + import java.sql.ResultSet; + import java.sql.Statement; + + public class UserRepository { + public ResultSet findById(Connection conn, String userIdFromRequest) throws Exception { + // userIdFromRequest is untrusted (source can be Servlet, Spring, etc.) + String sql = "SELECT * FROM users WHERE id = '" + userIdFromRequest + "'"; + + // VULNERABLE: tainted data reaches SQL execution API + Statement st = conn.createStatement(); + return st.executeQuery(sql); } } ``` - To remediate this issue, always use parameterized queries (e.g., `PreparedStatement`) - instead of string concatenation, and validate input where appropriate. - This prevents user input from being interpreted as SQL code. - - Safe code sample using `PreparedStatement`: + Safe example: ```java - public class UserProfileServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String userId = request.getParameter("userId"); - - // Example of simple input validation (optional but recommended) - if (userId == null || !userId.matches("\\d+")) { // expect numeric ID - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid user id."); - return; - } + import java.sql.Connection; + import java.sql.PreparedStatement; + import java.sql.ResultSet; - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement( - "SELECT * FROM users WHERE id = ?")) { + public class UserRepository { + public ResultSet findById(Connection conn, String userIdFromRequest) throws Exception { + String sql = "SELECT * FROM users WHERE id = ?"; - ps.setInt(1, Integer.parseInt(userId)); // bind parameter - ResultSet rs = ps.executeQuery(); - - // ... process result set safely ... - - } catch (SQLException e) { - throw new ServletException(e); - } + PreparedStatement ps = conn.prepareStatement(sql); + ps.setString(1, userIdFromRequest); // bound as data, not SQL syntax + return ps.executeQuery(); } } ``` - Additional best practices include using least-privilege database accounts, avoiding constructing dynamic SQL where possible, - and centralizing database access logic to enforce safe patterns throughout the application. + Key vulnerable patterns covered by this rule include JDBC statement execution, Spring JDBC operations, + and dynamic JPA/Hibernate query creation when untrusted input controls query text. references: - https://owasp.org/www-community/attacks/SQL_Injection - https://en.wikipedia.org/wiki/SQL_injection @@ -91,141 +63,11 @@ rules: join: refs: - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/spring/jdbc-sqli-sinks.yaml#spring-sqli-sink - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: sql-injection-in-spring-app - severity: ERROR - message: >- - Potential SQL injection: detected unescaped input from a user-manipulated data source going into a SQL sink or statement - metadata: - cwe: CWE-89 - short-description: Potential SQL injection - full-description: |- - SQL injection is a vulnerability where untrusted input is concatenated into SQL queries and - sent to the database without proper validation or parameterization. In Spring-based applications - (e.g., Spring MVC + JdbcTemplate or Spring Data/JPA), this typically occurs when request parameters - or method arguments are directly embedded into SQL or JPQL/HQL strings. An attacker can craft input - that alters the query's structure, leading to data theft, data manipulation, or full database compromise. - - Vulnerable code sample - - ```java - // Example: Spring MVC controller using JdbcTemplate insecurely - - @RestController - @RequestMapping("/users") - public class UserController { - - private final JdbcTemplate jdbcTemplate; - - public UserController(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - @GetMapping("/search") - public User getUser(@RequestParam String username) { - // Vulnerable: username is concatenated directly into the SQL query - String sql = "SELECT id, username, email FROM users WHERE username = '" + username + "'"; - - return jdbcTemplate.queryForObject( - sql, - (rs, rowNum) -> new User( - rs.getLong("id"), - rs.getString("username"), - rs.getString("email") - ) - ); - } - } - ``` - - To remediate this issue, always use parameterized queries - (e.g., `JdbcTemplate` with `?` placeholders, `PreparedStatement`, or Spring Data JPA method queries) - instead of string concatenation, and validate input where appropriate. This ensures user input is treated as data, not executable SQL. - - Safe code sample using `JdbcTemplate` with parameters: - - ```java - @RestController - @RequestMapping("/users") - public class UserController { - - private final JdbcTemplate jdbcTemplate; - - public UserController(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - @GetMapping("/search") - public User getUser(@RequestParam String username) { - // Optional but recommended: validate/normalize input - if (username == null || username.isBlank()) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid username"); - } - - String sql = "SELECT id, username, email FROM users WHERE username = ?"; - - return jdbcTemplate.queryForObject( - sql, - new Object[]{username}, // bind parameter - (rs, rowNum) -> new User( - rs.getLong("id"), - rs.getString("username"), - rs.getString("email") - ) - ); - } - } - ``` - - Safe code sample using Spring Data JPA (also parameterized under the hood): - - ```java - public interface UserRepository extends JpaRepository { - - // Derived query method — parameters are safely bound - Optional findByUsername(String username); - } - - @RestController - @RequestMapping("/users") - public class UserController { - - private final UserRepository userRepository; - - public UserController(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @GetMapping("/search") - public User getUser(@RequestParam String username) { - return userRepository.findByUsername(username) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); - } - } - ``` - - Additional best practices include using least-privilege database accounts, avoiding dynamic SQL/JPQL when possible, and centralizing data access in repositories or service layers that enforce safe patterns throughout the Spring application. - references: - - https://owasp.org/www-community/attacks/SQL_Injection - - https://en.wikipedia.org/wiki/SQL_injection - license: LGPL 2.1 (GNU Lesser General Public License, Version 2.1) - provenance: - - https://find-sec-bugs.github.io/bugs.htm#SQL_INJECTION - - https://github.com/semgrep/semgrep-rules/blob/develop/java/spring/security/audit/spring-sqli.yaml - - https://gitlab.com/gitlab-org/security-products/sast-rules/-/blob/main/rules/lgpl-cc/java/inject/rule-SqlInjection.yml - languages: - - java - mode: join - join: - refs: + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/spring/jdbc-sqli-sinks.yaml#spring-sqli-sink as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' diff --git a/rules/java/security/ssrf.yaml b/rules/java/security/ssrf.yaml index 0dea82d..bea057b 100644 --- a/rules/java/security/ssrf.yaml +++ b/rules/java/security/ssrf.yaml @@ -1,5 +1,5 @@ rules: - - id: ssrf-in-servlet-app + - id: ssrf severity: ERROR message: >- Potential SSRF: the web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, @@ -8,143 +8,59 @@ rules: cwe: CWE-918 short-description: Potential server-side request forgery (SSRF) full-description: |- - Server-Side Request Forgery (SSRF) in Java servlet applications occurs when the server makes HTTP or other network requests based on user‑supplied input (such as a URL) without proper validation. An attacker can abuse this to make the server connect to internal services (e.g., `http://127.0.0.1:8080/`, cloud metadata services, or other internal hosts) that are not otherwise exposed to the internet, potentially leading to data exfiltration, port scanning, or further compromise. + SSRF occurs when untrusted input reaches outbound network APIs without strict destination controls. + This rule focuses on calls such as URL/URI connection methods, `RestTemplate` requests, + socket address creation, and URL-based DB connection entrypoints. - Vulnerable code sample: + Vulnerable example: ```java - import javax.servlet.ServletException; - import javax.servlet.http.HttpServlet; - import javax.servlet.http.HttpServletRequest; - import javax.servlet.http.HttpServletResponse; - import java.io.*; - import java.net.HttpURLConnection; import java.net.URL; - public class ProxyServlet extends HttpServlet { + public class FetchService { + public String fetch(String targetFromRequest) throws Exception { + // targetFromRequest is untrusted + URL url = new URL(targetFromRequest); - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - // User controls the full URL - String targetUrl = request.getParameter("url"); - if (targetUrl == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing 'url' parameter"); - return; - } - - // Dangerous: directly using unvalidated user input as a target URL - URL url = new URL(targetUrl); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - - response.setStatus(conn.getResponseCode()); - try (InputStream in = conn.getInputStream(); - OutputStream out = response.getOutputStream()) { - byte[] buffer = new byte[4096]; - int len; - while ((len = in.read(buffer)) != -1) { - out.write(buffer, 0, len); - } - } + // VULNERABLE + return new String(url.openStream().readAllBytes()); } } ``` - To remediate this issue, avoid letting clients control arbitrary URLs. Prefer designing APIs so that clients pass identifiers or resource names, not full addresses, and resolve those to known safe locations on the server side. If you truly must fetch external URLs, strictly validate and constrain where the server is allowed to connect (e.g., protocol, host allowlist, and blocking internal IP ranges). - - Safer pattern using an allowlist of hosts and restricting the scheme: + Safe example: ```java - import javax.servlet.ServletException; - import javax.servlet.http.*; - import java.io.*; - import java.net.*; - import java.nio.charset.StandardCharsets; + import java.net.InetAddress; + import java.net.URI; import java.util.Set; - public class SafeProxyServlet extends HttpServlet { - - // Only allow requests to these external hosts - private static final Set ALLOWED_HOSTS = Set.of( - "api.example.com", - "services.partner.com" - ); - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String targetUrl = request.getParameter("url"); - if (targetUrl == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing 'url' parameter"); - return; - } - - URI uri; - try { - uri = new URI(targetUrl); - } catch (URISyntaxException e) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid URL"); - return; - } + public class FetchService { + private static final Set ALLOWED_HOSTS = Set.of("api.example.com", "partner.example.com"); - // 1. Restrict scheme - String scheme = uri.getScheme(); - if (scheme == null || - !(scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unsupported scheme"); - return; + public URI validate(String targetFromRequest) throws Exception { + URI uri = new URI(targetFromRequest); + if (!"https".equalsIgnoreCase(uri.getScheme())) { + throw new IllegalArgumentException("Only HTTPS allowed"); } - // 2. Restrict host to an allowlist String host = uri.getHost(); if (host == null || !ALLOWED_HOSTS.contains(host.toLowerCase())) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Host not allowed"); - return; + throw new SecurityException("Host not allowed"); } - // 3. Optional: resolve host and reject private/internal IP ranges - InetAddress address = InetAddress.getByName(host); - if (address.isAnyLocalAddress() || - address.isLoopbackAddress() || - address.isSiteLocalAddress()) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Internal addresses are not allowed"); - return; + InetAddress addr = InetAddress.getByName(host); + if (addr.isAnyLocalAddress() || addr.isLoopbackAddress() || addr.isSiteLocalAddress()) { + throw new SecurityException("Internal addresses are forbidden"); } - URL url = uri.toURL(); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setConnectTimeout(5000); - conn.setReadTimeout(5000); - conn.setRequestMethod("GET"); - - int status = conn.getResponseCode(); - response.setStatus(status); - - InputStream in = (status >= 200 && status < 400) - ? conn.getInputStream() - : conn.getErrorStream(); - - if (in != null) { - try (in; OutputStream out = response.getOutputStream()) { - byte[] buffer = new byte[4096]; - int len; - while ((len = in.read(buffer)) != -1) { - out.write(buffer, 0, len); - } - } - } + return uri; } } ``` - Additional best practices include: - - - Prefer mapping user input to predefined backend endpoints (e.g., `?reportId=123` instead of `?url=http://...`), never giving direct control over full URLs. - - Enforce network-level controls (firewall rules, security groups) to prevent application servers from reaching sensitive internal services (e.g., cloud metadata endpoints). - - Disable or tightly restrict any generic “URL fetch” or “proxy” functionality unless it is strictly required. + Key vulnerable patterns covered by this rule include `URL.open*`, `URI.create(...).toURL()`, + `RestTemplate` request methods with tainted URI/string targets, and tainted socket/connection targets. references: - https://owasp.org/www-community/attacks/Server_Side_Request_Forgery - https://portswigger.net/web-security/ssrf @@ -155,141 +71,14 @@ rules: join: refs: - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/ssrf-sinks.yaml#java-ssrf-sink - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: ssrf-in-spring-app - severity: ERROR - message: >- - Potential SSRF: the web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, - but it does not sufficiently ensure that the request is being sent to the expected destination. - metadata: - cwe: CWE-918 - short-description: Potential server-side request forgery (SSRF) - full-description: |- - Server-Side Request Forgery (SSRF) in Spring-based applications (e.g., Spring MVC / Spring Boot) - occurs when controllers or services make outbound HTTP (or other network) requests using user-controlled input - (such as a URL) without strict validation or restriction. Attackers can exploit this to make the application server connect to internal or otherwise protected services (e.g., `http://127.0.0.1:8080/`, cloud metadata services, internal admin panels), leading to data exposure, internal port scanning, or further compromise. - - Vulnerable code sample: - - ```java - import org.springframework.http.ResponseEntity; - import org.springframework.web.bind.annotation.*; - import org.springframework.web.client.RestTemplate; - - @RestController - @RequestMapping("/proxy") - public class ProxyController { - - private final RestTemplate restTemplate = new RestTemplate(); - - @GetMapping - public ResponseEntity proxy(@RequestParam("url") String targetUrl) { - // User controls full URL (e.g., https://evil.com?url=http://127.0.0.1:8080/admin) - if (targetUrl == null || targetUrl.isBlank()) { - return ResponseEntity.badRequest().body("Missing 'url' parameter"); - } - - // Vulnerable: directly using unvalidated user input as the target - String body = restTemplate.getForObject(targetUrl, String.class); - return ResponseEntity.ok(body); - } - } - ``` - - To remediate this issue, avoid letting clients control arbitrary URLs. Instead, design APIs so that clients pass logical identifiers or resource names, which your server maps to predefined, trusted endpoints. If you must fetch external URLs based on user input, strictly validate and constrain where the server may connect (e.g., use an allowlist of domains, restrict schemes to HTTP/HTTPS, and block internal IP ranges). - - Safer code sample using an allowlist and basic IP checks: - - ```java - import org.springframework.http.ResponseEntity; - import org.springframework.web.bind.annotation.*; - import org.springframework.web.client.RestTemplate; - - import java.net.*; - import java.util.Set; - - @RestController - @RequestMapping("/safe-proxy") - public class SafeProxyController { - - private final RestTemplate restTemplate = new RestTemplate(); - - // Only allow outbound requests to these hosts - private static final Set ALLOWED_HOSTS = Set.of( - "api.example.com", - "services.partner.com" - ); - - @GetMapping - public ResponseEntity proxy(@RequestParam("url") String targetUrl) { - if (targetUrl == null || targetUrl.isBlank()) { - return ResponseEntity.badRequest().body("Missing 'url' parameter"); - } - - URI uri; - try { - uri = new URI(targetUrl); - } catch (URISyntaxException e) { - return ResponseEntity.badRequest().body("Invalid URL"); - } - - // 1. Restrict scheme - String scheme = uri.getScheme(); - if (scheme == null || - !(scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) { - return ResponseEntity.badRequest().body("Unsupported scheme"); - } - - // 2. Restrict host to an allowlist - String host = uri.getHost(); - if (host == null || !ALLOWED_HOSTS.contains(host.toLowerCase())) { - return ResponseEntity.status(403).body("Host not allowed"); - } - - // 3. Resolve IP and block internal/loopback addresses - try { - InetAddress addr = InetAddress.getByName(host); - if (addr.isAnyLocalAddress() - || addr.isLoopbackAddress() - || addr.isSiteLocalAddress()) { - return ResponseEntity.status(403).body("Internal addresses are not allowed"); - } - } catch (UnknownHostException e) { - return ResponseEntity.badRequest().body("Unable to resolve host"); - } - - // 4. Perform the request to the validated URL - String body = restTemplate.getForObject(uri, String.class); - return ResponseEntity.ok(body); - } - } - ``` - - Additional Spring-focused recommendations: - - - Prefer patterns like `GET /reports/{id}` where `{id}` maps to known backend URLs in configuration or code, instead of `GET /proxy?url=...`. - - Centralize outbound HTTP access (e.g., through a service or a custom `RestTemplate` / `WebClient` bean) and enforce host/URL policies there. - - Combine application-level protections with network-level controls (firewalls, security groups, service mesh policies) to block access to internal services and metadata endpoints (e.g., `169.254.169.254` on cloud platforms). - references: - - https://owasp.org/www-community/attacks/Server_Side_Request_Forgery - - https://portswigger.net/web-security/ssrf - provenance: https://gitlab.com/gitlab-org/security-products/sast-rules/-/blob/main/java/ssrf/rule-SSRF.yml - languages: - - java - mode: join - join: - refs: + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/ssrf-sinks.yaml#java-ssrf-sink as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - id: java-servlet-parameter-pollution severity: ERROR diff --git a/rules/java/security/strings.yaml b/rules/java/security/strings.yaml index c1e67db..045ed0b 100644 --- a/rules/java/security/strings.yaml +++ b/rules/java/security/strings.yaml @@ -47,7 +47,7 @@ rules: ... java.text.Normalizer.normalize($VAR, ...); - - id: format-string-external-manipulation-in-servlet-app + - id: format-string-external-manipulation severity: WARNING message: >- The application allows user input to control format string parameters. By passing invalid format @@ -57,71 +57,48 @@ rules: cwe: CWE-134 short-description: Use of externally-controlled format string full-description: |- - The application allows user input to control format string parameters. By passing invalid format - string specifiers an adversary could cause the application to throw exceptions or possibly leak - internal information depending on application logic. + Format-string misuse occurs when untrusted data reaches a formatting API as the format template + (for example `String.format($FORMAT, ...)`, `Formatter.format(...)`, or `printf(...)`). - Never allow user-supplied input to be used to create a format string. Replace all format string - arguments with hardcoded format strings containing the necessary specifiers. + Vulnerable example: - Example of using `String.format` safely: - ``` - // Get untrusted user input - String userInput = request.getParameter("someInput"); - // Ensure that user input is not included in the first argument to String.format - String.format("Hardcoded string expecting a string: %s", userInput); - // ... + ```java + public class MessageService { + public String render(String formatFromRequest, String userValue) { + // formatFromRequest is untrusted and controls the format string + return String.format(formatFromRequest, userValue); // VULNERABLE + } + } ``` - provenance: https://gitlab.com/gitlab-org/security-products/sast-rules/-/blob/main/java/strings/rule-FormatStringManipulation.yml - languages: - - java - mode: join - join: - refs: - - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/security/strings.yaml#java-string-format-sinks - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$FORMAT_STR' - - - id: format-string-external-manipulation-in-spring-app - severity: WARNING - message: >- - The application allows user input to control format string parameters. By passing invalid format - string specifiers an adversary could cause the application to throw exceptions or possibly leak - internal information depending on application logic. - metadata: - cwe: CWE-134 - short-description: Use of externally-controlled format string - full-description: |- - The application allows user input to control format string parameters. By passing invalid format - string specifiers an adversary could cause the application to throw exceptions or possibly leak - internal information depending on application logic. - Never allow user-supplied input to be used to create a format string. Replace all format string - arguments with hardcoded format strings containing the necessary specifiers. + Safe example: - Example of using `String.format` safely: - ``` - // Get untrusted user input - String userInput = request.getParameter("someInput"); - // Ensure that user input is not included in the first argument to String.format - String.format("Hardcoded string expecting a string: %s", userInput); - // ... + ```java + public class MessageService { + public String render(String formatFromRequest, String userValue) { + // Keep format string constant; use untrusted data only as arguments + return String.format("User value: %s", userValue); + } + } ``` + + Key vulnerable patterns covered by this rule include `String.format`, `Formatter.format`, + `PrintStream.printf/format`, and `System.out.printf/format` when template text is tainted. provenance: https://gitlab.com/gitlab-org/security-products/sast-rules/-/blob/main/java/strings/rule-FormatStringManipulation.yml languages: - java mode: join join: refs: + - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/security/strings.yaml#java-string-format-sinks as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$FORMAT_STR' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$FORMAT_STR' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$FORMAT_STR' - id: java-string-format-sinks options: diff --git a/rules/java/security/unsafe-deserialization.yaml b/rules/java/security/unsafe-deserialization.yaml index 01454ca..4db652b 100644 --- a/rules/java/security/unsafe-deserialization.yaml +++ b/rules/java/security/unsafe-deserialization.yaml @@ -1,5 +1,5 @@ rules: - - id: unsafe-object-mapper-in-servlet-app + - id: unsafe-object-mapper severity: ERROR message: >- Found object deserialization using ObjectInputStream with user-controlled input. @@ -8,6 +8,13 @@ rules: metadata: cwe: CWE-502 short-description: Deserialization of untrusted data with ObjectMapper + full-description: |- + Unsafe object deserialization occurs when untrusted data is deserialized with ObjectInputStream or equivalent object stream APIs. + In Servlet and Spring applications, request-controlled byte streams must not be deserialized into arbitrary object graphs. + Attackers can trigger gadget chains during deserialization and achieve remote code execution or severe integrity impact. + + Do not deserialize untrusted native Java objects. Prefer safer formats and explicit data transfer object mappings. + If legacy deserialization is unavoidable, enforce strict class allowlists and isolate deserialization boundaries. references: - https://find-sec-bugs.github.io/bugs.htm#OBJECT_DESERIALIZATION provenance: https://find-sec-bugs.github.io/bugs.htm#OBJECT_DESERIALIZATION @@ -17,35 +24,14 @@ rules: join: refs: - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java-unsafe-object-mapper-sink - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: unsafe-object-mapper-in-spring-app - severity: ERROR - message: >- - Found object deserialization using ObjectInputStream with user-controlled input. - Deserialization of entire Java objects is dangerous because malicious actors can - create Java object streams with unintended consequences. - metadata: - cwe: CWE-502 - short-description: Deserialization of untrusted data with ObjectMapper - references: - - https://find-sec-bugs.github.io/bugs.htm#OBJECT_DESERIALIZATION - provenance: https://find-sec-bugs.github.io/bugs.htm#OBJECT_DESERIALIZATION - languages: - - java - mode: join - join: - refs: + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java-unsafe-object-mapper-sink as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - id: java-unsafe-object-mapper-sink options: @@ -92,13 +78,20 @@ rules: - pattern: $X = $Y.getObject(...); - - id: unsafe-jackson-deserialization-in-servlet-app + - id: unsafe-jackson-deserialization severity: ERROR message: Found Jackson deserialization with user-controlled input. metadata: cwe: - CWE-502 short-description: Unsafe Jackson deserialization with user-controlled input + full-description: |- + Unsafe Jackson deserialization occurs when untrusted JSON is deserialized into dangerous or overly broad target types. + In Servlet and Spring applications, this can happen when request bodies are bound to polymorphic types without strict controls. + Attackers may instantiate unexpected classes or trigger gadget-based behavior depending on mapper configuration and dependencies. + + Deserialize into explicit, fixed DTO classes and avoid permissive polymorphic typing for untrusted data. + Keep Jackson and dependent libraries updated, and validate request payload shape before deserialization. references: - https://swapneildash.medium.com/understanding-insecure-implementation-of-jackson-deserialization-7b3d409d2038 - https://cowtowncoder.medium.com/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062 @@ -110,35 +103,14 @@ rules: join: refs: - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/unsafe-deserialization-sinks.yaml#jackson-unsafe-deserialization - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$JSON' - - - id: unsafe-jackson-deserialization-in-spring-app - severity: ERROR - message: Found Jackson deserialization with user-controlled input. - metadata: - cwe: - - CWE-502 - short-description: Unsafe Jackson deserialization with user-controlled input - references: - - https://swapneildash.medium.com/understanding-insecure-implementation-of-jackson-deserialization-7b3d409d2038 - - https://cowtowncoder.medium.com/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062 - - https://adamcaudill.com/2017/10/04/exploiting-jackson-rce-cve-2017-7525/ - provenance: https://semgrep.dev/r/gitlab.java_deserialization_rule-JacksonUnsafeDeserialization - languages: - - java - mode: join - join: - refs: + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/unsafe-deserialization-sinks.yaml#jackson-unsafe-deserialization as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$JSON' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$JSON' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$JSON' - id: server-dangerous-object-deserialization severity: ERROR @@ -150,7 +122,7 @@ rules: metadata: cwe: - CWE-502 - short-description: Insecure deserelization in Java RMI + short-description: Insecure deserialization in Java RMI references: - https://frohoff.github.io/appseccali-marshalling-pickles/ - https://book.hacktricks.xyz/network-services-pentesting/1099-pentesting-java-rmi @@ -169,14 +141,21 @@ rules: metavariable: $PARAMTYPE regex: ^(?!(Integer|Long|Float|Double|Char|Boolean|int|long|float|double|char|boolean|String)) - - id: java-servlet-unsafe-snake-yaml-deserialization + - id: unsafe-snake-yaml-deserialization severity: ERROR message: >- - Insecure deserialization. Used SnakeYAML org.yaml.snakeyaml.Yaml() constructor on user-manipulater yaml data. + Insecure deserialization. Used SnakeYAML org.yaml.snakeyaml.Yaml() constructor on user-manipulated YAML data. metadata: cwe: - CWE-502 short-description: Insecure deserialization of untrusted YAML data + full-description: |- + Unsafe SnakeYAML deserialization occurs when untrusted YAML is loaded with constructors that instantiate arbitrary object types. + In Servlet and Spring applications, request data parsed this way can create dangerous objects and trigger unsafe code paths. + This can result in remote code execution or denial of service in vulnerable configurations. + + Do not load untrusted YAML into arbitrary object graphs. Use safe loader options and map input to simple, explicit data structures. + Disable features that allow implicit type instantiation from attacker-controlled YAML tags. references: - https://securitylab.github.com/research/swagger-yaml-parser-vulnerability/#snakeyaml-deserialization-vulnerability provenance: https://github.com/semgrep/semgrep-rules/blob/develop/java/lang/security/use-snakeyaml-constructor.yaml @@ -186,34 +165,14 @@ rules: join: refs: - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/unsafe-deserialization-sinks.yaml#yaml-unsafe-deserialization-sink - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$YAML' - - - id: spring-unsafe-snake-yaml-deserialization - severity: ERROR - message: >- - Insecure deserialization. Used SnakeYAML org.yaml.snakeyaml.Yaml() constructor on user-manipulater yaml data. - metadata: - cwe: - - CWE-502 - short-description: Insecure deserialization of untrusted YAML data - references: - - https://securitylab.github.com/research/swagger-yaml-parser-vulnerability/#snakeyaml-deserialization-vulnerability - provenance: https://github.com/semgrep/semgrep-rules/blob/develop/java/lang/security/use-snakeyaml-constructor.yaml - languages: - - java - mode: join - join: - refs: + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/unsafe-deserialization-sinks.yaml#yaml-unsafe-deserialization-sink as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$YAML' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$YAML' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$YAML' - id: insecure-resteasy-deserialization severity: WARNING diff --git a/rules/java/security/xxe.yaml b/rules/java/security/xxe.yaml index 72368a7..3af9245 100644 --- a/rules/java/security/xxe.yaml +++ b/rules/java/security/xxe.yaml @@ -1,5 +1,5 @@ rules: - - id: xxe-in-servlet-app + - id: xxe severity: ERROR message: >- Potential XXE: untrusted input is parsed as an XML with safety flags disabled @@ -7,115 +7,53 @@ rules: cwe: CWE-611 short-description: XML parsing of an untrusted input full-description: |- - XML External Entity (XXE) is a vulnerability that occurs when an application parses untrusted - XML with a parser that allows external entities or DTDs. An attacker can craft XML that causes - the server to read local files, make HTTP requests to internal systems (SSRF), or consume excessive resources (DoS). - In Java servlet applications, this often happens when request bodies are parsed as XML with default, insecure parser settings. + XXE occurs when untrusted XML reaches parser APIs without secure parser configuration + (for example `DocumentBuilder.parse(...)`, `SAXParser.parse(...)`, or StAX readers). - ```java - // Vulnerable servlet code: parses XML from the request with default settings - - import jakarta.servlet.ServletException; - import jakarta.servlet.http.HttpServlet; - import jakarta.servlet.http.HttpServletRequest; - import jakarta.servlet.http.HttpServletResponse; + Vulnerable example: - import org.w3c.dom.Document; + ```java import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; - import java.io.IOException; - - public class XmlUploadServlet extends HttpServlet { - - @Override - protected void doPost(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - - try { - // DEFAULT configuration — vulnerable to XXE - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); + import org.w3c.dom.Document; - // Attacker-controlled XML from the request body - Document doc = builder.parse(request.getInputStream()); + public class XmlService { + public Document parse(java.io.InputStream xmlFromRequest) throws Exception { + DocumentBuilderFactory f = DocumentBuilderFactory.newInstance(); + DocumentBuilder b = f.newDocumentBuilder(); - // Process the XML document... - } catch (Exception e) { - throw new ServletException(e); - } + // VULNERABLE: parse called with insecure defaults + return b.parse(xmlFromRequest); } } ``` - To remediate this issue, configure XML parsers to disallow DTDs and external entities, - enable secure processing features, and upgrade to a recent JDK/XML library version that - supports these controls. Where possible, avoid XML or use data formats/parsers that do not support external entities. + Safe example: ```java - // Safe servlet code: XXE protections enabled - - import jakarta.servlet.ServletException; - import jakarta.servlet.http.HttpServlet; - import jakarta.servlet.http.HttpServletRequest; - import jakarta.servlet.http.HttpServletResponse; - - import org.w3c.dom.Document; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; - import java.io.IOException; - - public class SafeXmlUploadServlet extends HttpServlet { - - @Override - protected void doPost(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - - // Enable secure processing - factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - - // Completely disallow DOCTYPE declarations - factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - - // Disable external entities - 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); - - // Additional hardening - factory.setXIncludeAware(false); - factory.setExpandEntityReferences(false); - - // For JDKs that support it: block all external access - try { - factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - } catch (IllegalArgumentException ignored) { - // Attributes not supported in older JDKs; safe to ignore - } - - DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.parse(request.getInputStream()); + import org.w3c.dom.Document; - // Safely process the XML document... - } catch (Exception e) { - throw new ServletException(e); - } + public class XmlService { + public Document parse(java.io.InputStream xmlFromRequest) throws Exception { + DocumentBuilderFactory f = DocumentBuilderFactory.newInstance(); + f.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + f.setFeature("http://xml.org/sax/features/external-general-entities", false); + f.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + f.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + f.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + + DocumentBuilder b = f.newDocumentBuilder(); + return b.parse(xmlFromRequest); } } ``` - Additional recommended steps: - - - Validate and constrain inputs (e.g., size limits on request bodies). - - Prefer libraries or APIs that do not support DTD/entities when possible. - - Turn off XML features you do not need (schema validation, XInclude, etc.). - - Consider switching to JSON if XML's advanced features are unnecessary. + Key vulnerable patterns covered by this rule include DOM/SAX/StAX/XMLDecoder/Transformer parsing and transformation + calls when safe XXE-hardening flags are absent. references: - https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html - https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing @@ -129,101 +67,11 @@ rules: join: refs: - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source - as: untrusted-data - - rule: java/lib/generic/xxe-sinks.yaml#java-xxe-sinks - as: sink - on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' - - - id: xxe-in-spring-app - severity: ERROR - message: >- - Potential XXE: untrusted input is parsed as an XML with safety flags disabled - metadata: - cwe: CWE-611 - short-description: XML parsing of an untrusted input - full-description: |- - XML External Entity (XXE) in Spring applications occurs when the application parses XML - that comes (directly or indirectly) from users, and the XML parser is allowed to process - external entities or DTDs. An attacker can supply a crafted XML document that causes the parser to: - - - Read local files (e.g., configuration, secrets). - - Make arbitrary HTTP requests from the server (SSRF). - - Potentially trigger denial of service. - - This often happens in Spring MVC / Spring Boot apps when developers manually use low-level XML APIs - (e.g., `DocumentBuilderFactory`, `SAXParserFactory`) with unsafe defaults, or when XML marshalling libraries are not hardened. - - ```java - // Vulnerable code sample (Spring Boot / Spring MVC) - - @RestController - @RequestMapping("/api") - public class XmlController { - - @PostMapping(value = "/process-xml", consumes = MediaType.APPLICATION_XML_VALUE) - public String processXml(@RequestBody String xml) throws Exception { - // Insecure: default configuration may allow DTDs and external entities - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = dbf.newDocumentBuilder(); - - Document doc = builder.parse(new InputSource(new StringReader(xml))); - - // Application logic using parsed XML... - String value = doc.getElementsByTagName("name").item(0).getTextContent(); - return "Received: " + value; - } - } - ``` - - To remediate this issue, configure all XML parsers in the Spring application to reject - DTDs and external entities, or avoid XML entirely if possible (use JSON, for example). - With JAXP/DOM, harden `DocumentBuilderFactory` as follows: - - ```java - // Safe XML parsing configuration - - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - - // Completely disallow DOCTYPE declarations (strong protection against XXE) - dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - - // Disable external entities - dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); - dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - - // Extra hardening - dbf.setXIncludeAware(false); - dbf.setExpandEntityReferences(false); - - DocumentBuilder builder = dbf.newDocumentBuilder(); - Document doc = builder.parse(new InputSource(new StringReader(xml))); - ``` - - Additional Spring-specific guidelines: - - - Prefer higher-level, framework-managed XML handling where Spring already applies - secure defaults, and verify the underlying parser's XXE settings (JAXP, JAXB, Jackson XML, etc.). - - Disable DTDs and external entities in any other XML-related factories - (`SAXParserFactory`, `XMLInputFactory`, etc.) used in beans or libraries within the Spring context. - - If your code uses third-party XML libraries, consult their documentation for XXE-safe configuration - and ensure those beans are created/configured securely in your Spring configuration. - references: - - https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html - - https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing - provenance: - - https://github.com/semgrep/semgrep-rules/tree/develop/java/lang/security/audit/xxe - - https://github.com/semgrep/semgrep-rules/blob/develop/java/lang/security/audit/xml-decoder.yaml - - https://gitlab.com/gitlab-org/security-products/sast-rules/-/blob/main/java/xml/rule-XsltTransform.yml - languages: - - java - mode: join - join: - refs: + as: servlet-untrusted-data - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source - as: untrusted-data + as: spring-untrusted-data - rule: java/lib/generic/xxe-sinks.yaml#java-xxe-sinks as: sink on: - - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'servlet-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + - 'spring-untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' diff --git a/test/src/main/java/security/codeinjection/GroovyInjectionServletSamples.java b/test/src/main/java/security/codeinjection/GroovyInjectionServletSamples.java index 4439f65..e70de4d 100644 --- a/test/src/main/java/security/codeinjection/GroovyInjectionServletSamples.java +++ b/test/src/main/java/security/codeinjection/GroovyInjectionServletSamples.java @@ -22,7 +22,7 @@ public class GroovyInjectionServletSamples { public static class UnsafeGroovyServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "groovy-injection-in-servlet-app") + @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "groovy-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Attacker controls this parameter (e.g., ?script=...) @@ -41,7 +41,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) public static class SafeGroovyServlet extends HttpServlet { @Override - @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "groovy-injection-in-servlet-app") + @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "groovy-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { diff --git a/test/src/main/java/security/codeinjection/GroovyInjectionSpringSamples.java b/test/src/main/java/security/codeinjection/GroovyInjectionSpringSamples.java index 1116e69..b27d4e8 100644 --- a/test/src/main/java/security/codeinjection/GroovyInjectionSpringSamples.java +++ b/test/src/main/java/security/codeinjection/GroovyInjectionSpringSamples.java @@ -17,7 +17,7 @@ public class GroovyInjectionSpringSamples { public static class UnsafeGroovyController { @GetMapping("/groovy-injection-in-spring/unsafe") - @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "groovy-injection-in-spring-app") + @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "groovy-injection") public String unsafeGroovy(@RequestParam("script") String script) { GroovyShell shell = new GroovyShell(); @@ -32,7 +32,7 @@ public String unsafeGroovy(@RequestParam("script") String script) { public static class SafeGroovyController { @GetMapping("/groovy-injection-in-spring/safe") - @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "groovy-injection-in-spring-app") + @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "groovy-injection") public String safeGroovy(@RequestParam(value = "action", required = false) String action) { // Safer pattern: map user-controlled input to a fixed set of allowed operations, // without evaluating arbitrary Groovy code. diff --git a/test/src/main/java/security/codeinjection/OgnlInjectionServletSamples.java b/test/src/main/java/security/codeinjection/OgnlInjectionServletSamples.java index 7d7ce88..2e8776c 100644 --- a/test/src/main/java/security/codeinjection/OgnlInjectionServletSamples.java +++ b/test/src/main/java/security/codeinjection/OgnlInjectionServletSamples.java @@ -24,7 +24,7 @@ public class OgnlInjectionServletSamples { public static class UnsafeOgnlServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "ognl-injection-in-servlet-app") + @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "ognl-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Attacker controls the "expr" parameter @@ -52,7 +52,7 @@ public static class SafeOgnlServlet extends HttpServlet { private final UserService userService = new UserService(); @Override - @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "ognl-injection-in-servlet-app") + @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "ognl-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { diff --git a/test/src/main/java/security/codeinjection/OgnlInjectionSpringSamples.java b/test/src/main/java/security/codeinjection/OgnlInjectionSpringSamples.java index 58e204e..f67d54c 100644 --- a/test/src/main/java/security/codeinjection/OgnlInjectionSpringSamples.java +++ b/test/src/main/java/security/codeinjection/OgnlInjectionSpringSamples.java @@ -20,7 +20,7 @@ public class OgnlInjectionSpringSamples { public static class UnsafeOgnlController { @GetMapping("/ognl-injection-in-spring/unsafe") - @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "ognl-injection-in-spring-app") + @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "ognl-injection") public String unsafeOgnl(@RequestParam("expr") String expr) throws Exception { // Build OGNL context from application objects Map context = new HashMap<>(); @@ -40,7 +40,7 @@ public static class SafeOgnlController { private final UserService userService = new UserService(); @GetMapping("/ognl-injection-in-spring/safe") - @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "ognl-injection-in-spring-app") + @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "ognl-injection") public String safeOgnl(@RequestParam(value = "action", required = false) String action, @RequestParam(value = "userId", required = false) String userId) { // Safer approach: use whitelisted actions instead of evaluating OGNL expressions. diff --git a/test/src/main/java/security/codeinjection/ScriptEngineInjectionServletSamples.java b/test/src/main/java/security/codeinjection/ScriptEngineInjectionServletSamples.java index 7e21d75..f951506 100644 --- a/test/src/main/java/security/codeinjection/ScriptEngineInjectionServletSamples.java +++ b/test/src/main/java/security/codeinjection/ScriptEngineInjectionServletSamples.java @@ -26,7 +26,7 @@ public class ScriptEngineInjectionServletSamples { public static class UnsafeScriptEngineServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "script-engine-injection-in-servlet-app") + @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "script-engine-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String expr = request.getParameter("expr"); @@ -60,7 +60,7 @@ public SafeScriptEngineServlet() throws ScriptException { } @Override - @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "script-engine-injection-in-servlet-app") + @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "script-engine-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { diff --git a/test/src/main/java/security/codeinjection/ScriptEngineInjectionSpringSamples.java b/test/src/main/java/security/codeinjection/ScriptEngineInjectionSpringSamples.java index c3f9ed7..035786d 100644 --- a/test/src/main/java/security/codeinjection/ScriptEngineInjectionSpringSamples.java +++ b/test/src/main/java/security/codeinjection/ScriptEngineInjectionSpringSamples.java @@ -22,7 +22,7 @@ public class ScriptEngineInjectionSpringSamples { public static class UnsafeScriptEngineController { @GetMapping("/script-engine-injection-in-spring/unsafe") - @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "script-engine-injection-in-spring-app") + @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "script-engine-injection") public String unsafeScriptEngine(@RequestParam("expr") String expr) throws ScriptException { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("javascript"); @@ -49,7 +49,7 @@ public SafeScriptEngineController() throws ScriptException { } @GetMapping("/script-engine-injection-in-spring/safe") - @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "script-engine-injection-in-spring-app") + @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "script-engine-injection") public String safeScriptEngine(@RequestParam("a") int a, @RequestParam("b") int b) throws ScriptException { Bindings bindings = engine.createBindings(); bindings.put("a", a); diff --git a/test/src/main/java/security/codeinjection/SstiServletSamples.java b/test/src/main/java/security/codeinjection/SstiServletSamples.java index 1897121..6bea02b 100644 --- a/test/src/main/java/security/codeinjection/SstiServletSamples.java +++ b/test/src/main/java/security/codeinjection/SstiServletSamples.java @@ -28,7 +28,7 @@ public class SstiServletSamples { public static class UnsafeTemplateServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "ssti-in-servlet-app") + @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "ssti") protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Attacker controls the entire template content String templateSource = request.getParameter("messageTemplate"); @@ -56,7 +56,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) public static class SafeTemplateServlet extends HttpServlet { @Override - @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "ssti-in-servlet-app") + @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "ssti") protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ServletContext servletContext = getServletContext(); Configuration cfg = (Configuration) servletContext.getAttribute("freemarkerCfg"); diff --git a/test/src/main/java/security/codeinjection/TemplateInjectionSpringSamples.java b/test/src/main/java/security/codeinjection/TemplateInjectionSpringSamples.java index 1898dc5..523ef1b 100644 --- a/test/src/main/java/security/codeinjection/TemplateInjectionSpringSamples.java +++ b/test/src/main/java/security/codeinjection/TemplateInjectionSpringSamples.java @@ -33,7 +33,7 @@ public static class UnsafeTemplateController { private Configuration freemarkerConfiguration; @PostMapping("/unsafe") - @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "ssti-in-spring-app") + @PositiveRuleSample(value = "java/security/code-injection.yaml", id = "ssti") protected void previewUnsafe(HttpServletRequest request, HttpServletResponse response) throws ServletException { // Attacker controls the entire template content String templateSource = request.getParameter("messageTemplate"); @@ -62,7 +62,7 @@ public static class SafeTemplateServlet extends HttpServlet { private Configuration freemarkerConfiguration; @PostMapping("/safe") - @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "ssti-in-spring-app") + @NegativeRuleSample(value = "java/security/code-injection.yaml", id = "ssti") protected void previewSafe(HttpServletRequest request, HttpServletResponse response) throws ServletException { try { // Use only server-controlled template names (e.g., stored on disk) diff --git a/test/src/main/java/security/commandinjection/CommandInjectionServletSamples.java b/test/src/main/java/security/commandinjection/CommandInjectionServletSamples.java index 5097ce7..6487a75 100644 --- a/test/src/main/java/security/commandinjection/CommandInjectionServletSamples.java +++ b/test/src/main/java/security/commandinjection/CommandInjectionServletSamples.java @@ -24,7 +24,7 @@ public class CommandInjectionServletSamples { public static class UnsafeCommandServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/command-injection.yaml", id = "os-command-injection-in-servlet-app") + @PositiveRuleSample(value = "java/security/command-injection.yaml", id = "os-command-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String host = request.getParameter("host"); // untrusted input @@ -53,7 +53,7 @@ public static class SafeCommandServlet extends HttpServlet { @Override // TODO: restore this when conditional validators are implemented -// @NegativeRuleSample(value = "java/security/command-injection.yaml", id = "os-command-injection-in-servlet-app") +// @NegativeRuleSample(value = "java/security/command-injection.yaml", id = "os-command-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String host = request.getParameter("host"); diff --git a/test/src/main/java/security/commandinjection/CommandInjectionSpringSamples.java b/test/src/main/java/security/commandinjection/CommandInjectionSpringSamples.java index 7f1581d..a0a7290 100644 --- a/test/src/main/java/security/commandinjection/CommandInjectionSpringSamples.java +++ b/test/src/main/java/security/commandinjection/CommandInjectionSpringSamples.java @@ -21,7 +21,7 @@ public static class UnsafeCommandInjectionController { * executed via Runtime.exec. */ @GetMapping("/os-command-injection-in-spring/unsafe") - @PositiveRuleSample(value = "java/security/command-injection.yaml", id = "os-command-injection-in-spring-app") + @PositiveRuleSample(value = "java/security/command-injection.yaml", id = "os-command-injection") public String unsafePing(@RequestParam String host) { // VULNERABLE: direct concatenation of untrusted input into OS command String command = "ping -c 4 " + host; @@ -48,7 +48,7 @@ public static class SafeCommandInjectionController { @GetMapping("/os-command-injection-in-spring/safe") // TODO: restore this when conditional validators are implemented -// @NegativeRuleSample(value = "java/security/command-injection.yaml", id = "os-command-injection-in-spring-app") +// @NegativeRuleSample(value = "java/security/command-injection.yaml", id = "os-command-injection") public String safePing(@RequestParam String host) { // Strict validation / whitelisting of the host value if (host == null || !host.matches("^[a-zA-Z0-9._-]{1,255}$")) { diff --git a/test/src/main/java/security/crlfinjection/HttpResponseSplittingServletSamples.java b/test/src/main/java/security/crlfinjection/HttpResponseSplittingServletSamples.java index 9cec5de..e486cc2 100644 --- a/test/src/main/java/security/crlfinjection/HttpResponseSplittingServletSamples.java +++ b/test/src/main/java/security/crlfinjection/HttpResponseSplittingServletSamples.java @@ -22,7 +22,7 @@ public class HttpResponseSplittingServletSamples { public static class UnsafeHeaderServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/crlf-injection.yaml", id = "http-response-splitting-in-servlet-app") + @PositiveRuleSample(value = "java/security/crlf-injection.yaml", id = "http-response-splitting") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String user = request.getParameter("user"); // attacker-controlled @@ -44,7 +44,7 @@ public static class SafeHeaderServlet extends HttpServlet { @Override // TODO: restore this when conditional validators are implemented -// @NegativeRuleSample(value = "java/security/crlf-injection.yaml", id = "http-response-splitting-in-servlet-app") +// @NegativeRuleSample(value = "java/security/crlf-injection.yaml", id = "http-response-splitting") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String user = request.getParameter("user"); diff --git a/test/src/main/java/security/crlfinjection/HttpResponseSplittingSpringSamples.java b/test/src/main/java/security/crlfinjection/HttpResponseSplittingSpringSamples.java index 131bf87..4854b21 100644 --- a/test/src/main/java/security/crlfinjection/HttpResponseSplittingSpringSamples.java +++ b/test/src/main/java/security/crlfinjection/HttpResponseSplittingSpringSamples.java @@ -21,7 +21,7 @@ public static class UnsafeHttpResponseSplittingController { * Unsafe endpoint that uses untrusted input directly in headers and redirect URLs. */ @GetMapping("/http-response-splitting-in-spring/unsafe") - @PositiveRuleSample(value = "java/security/crlf-injection.yaml", id = "http-response-splitting-in-spring-app") + @PositiveRuleSample(value = "java/security/crlf-injection.yaml", id = "http-response-splitting") public void unsafe(@RequestParam(name = "user", required = false) String user, @RequestParam(name = "next", required = false) String next, HttpServletResponse response) throws IOException { @@ -50,7 +50,7 @@ public static class SafeHttpResponseSplittingController { */ @GetMapping("/http-response-splitting-in-spring/safe") // TODO: restore this when conditional validators are implemented -// @NegativeRuleSample(value = "java/security/crlf-injection.yaml", id = "http-response-splitting-in-spring-app") +// @NegativeRuleSample(value = "java/security/crlf-injection.yaml", id = "http-response-splitting") public void safe(@RequestParam(name = "user", required = false) String user, @RequestParam(name = "next", required = false) String next, HttpServletResponse response) throws IOException { diff --git a/test/src/main/java/security/crlfinjection/SmtpCrlfInjectionServletSamples.java b/test/src/main/java/security/crlfinjection/SmtpCrlfInjectionServletSamples.java index 2162ca3..77a844f 100644 --- a/test/src/main/java/security/crlfinjection/SmtpCrlfInjectionServletSamples.java +++ b/test/src/main/java/security/crlfinjection/SmtpCrlfInjectionServletSamples.java @@ -17,7 +17,7 @@ import java.util.Properties; /** - * Servlet samples for java-servlet-smtp-crlf-injection. + * Servlet samples for smtp-crlf-injection. */ public class SmtpCrlfInjectionServletSamples { @@ -30,7 +30,7 @@ private static Session getMailSession() { public static class UnsafeSmtpServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/crlf-injection.yaml", id = "java-servlet-smtp-crlf-injection") + @PositiveRuleSample(value = "java/security/crlf-injection.yaml", id = "smtp-crlf-injection") protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String to = request.getParameter("to"); @@ -66,7 +66,7 @@ private boolean containsCRLF(String value) { @Override // TODO: restore this when conditional validators are implemented -// @NegativeRuleSample(value = "java/security/crlf-injection.yaml", id = "java-servlet-smtp-crlf-injection") +// @NegativeRuleSample(value = "java/security/crlf-injection.yaml", id = "smtp-crlf-injection") protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String to = request.getParameter("to"); diff --git a/test/src/main/java/security/crlfinjection/SmtpCrlfInjectionSpringSamples.java b/test/src/main/java/security/crlfinjection/SmtpCrlfInjectionSpringSamples.java index 367adbb..67871d4 100644 --- a/test/src/main/java/security/crlfinjection/SmtpCrlfInjectionSpringSamples.java +++ b/test/src/main/java/security/crlfinjection/SmtpCrlfInjectionSpringSamples.java @@ -16,7 +16,7 @@ /** - * Spring MVC samples for spring-smtp-crlf-injection. + * Spring MVC samples for smtp-crlf-injection. */ public class SmtpCrlfInjectionSpringSamples { @@ -24,7 +24,7 @@ public class SmtpCrlfInjectionSpringSamples { public static class UnsafeSpringSmtpController { @PostMapping("/smtp-crlf/spring/unsafe") - @PositiveRuleSample(value = "java/security/crlf-injection.yaml", id = "spring-smtp-crlf-injection") + @PositiveRuleSample(value = "java/security/crlf-injection.yaml", id = "smtp-crlf-injection") public void unsafe(@RequestParam("to") String to, @RequestParam("subject") String subject, @RequestParam(value = "trackingId", required = false) String trackingId, @@ -58,7 +58,7 @@ private boolean containsCRLF(String value) { @PostMapping("/smtp-crlf/spring/safe") // TODO: restore this when conditional validators are implemented -// @NegativeRuleSample(value = "java/security/crlf-injection.yaml", id = "spring-smtp-crlf-injection") +// @NegativeRuleSample(value = "java/security/crlf-injection.yaml", id = "smtp-crlf-injection") public void safe(@RequestParam("to") String to, @RequestParam("subject") String subject, @RequestParam(value = "trackingId", required = false) String trackingId, diff --git a/test/src/main/java/security/dataqueryinjection/DataQueryInjectionServletSamples.java b/test/src/main/java/security/dataqueryinjection/DataQueryInjectionServletSamples.java index 9e305ad..b110446 100644 --- a/test/src/main/java/security/dataqueryinjection/DataQueryInjectionServletSamples.java +++ b/test/src/main/java/security/dataqueryinjection/DataQueryInjectionServletSamples.java @@ -32,8 +32,8 @@ /** * Servlet-based samples for data-query-injection rules: - * - xpath-injection-in-servlet-app - * - mongodb-injection-in-servlet-app + * - xpath-injection + * - mongodb-injection */ public class DataQueryInjectionServletSamples { @@ -47,7 +47,7 @@ public static class UnsafeXPathServlet extends HttpServlet { private final Document usersDoc = null; // simplified for sample; assume initialized elsewhere @Override - @PositiveRuleSample(value = "java/security/data-query-injection.yaml", id = "xpath-injection-in-servlet-app") + @PositiveRuleSample(value = "java/security/data-query-injection.yaml", id = "xpath-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); @@ -75,7 +75,7 @@ public static class SafeXPathServlet extends HttpServlet { private final Document usersDoc = null; // simplified for sample; assume initialized elsewhere @Override - @NegativeRuleSample(value = "java/security/data-query-injection.yaml", id = "xpath-injection-in-servlet-app") + @NegativeRuleSample(value = "java/security/data-query-injection.yaml", id = "xpath-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); @@ -117,7 +117,7 @@ public static class UnsafeMongoServlet extends HttpServlet { private final DB legacyDb = null; // simplified for sample; assume initialized elsewhere @Override - @PositiveRuleSample(value = "java/security/data-query-injection.yaml", id = "mongodb-injection-in-servlet-app") + @PositiveRuleSample(value = "java/security/data-query-injection.yaml", id = "mongodb-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); @@ -146,7 +146,7 @@ public static class SafeMongoServlet extends HttpServlet { @Override - @NegativeRuleSample(value = "java/security/data-query-injection.yaml", id = "mongodb-injection-in-servlet-app") + @NegativeRuleSample(value = "java/security/data-query-injection.yaml", id = "mongodb-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); diff --git a/test/src/main/java/security/dataqueryinjection/DataQueryInjectionSpringSamples.java b/test/src/main/java/security/dataqueryinjection/DataQueryInjectionSpringSamples.java index b99cb60..a3ba06b 100644 --- a/test/src/main/java/security/dataqueryinjection/DataQueryInjectionSpringSamples.java +++ b/test/src/main/java/security/dataqueryinjection/DataQueryInjectionSpringSamples.java @@ -13,8 +13,8 @@ /** * Spring MVC-style samples for data-query-injection rules: - * - xpath-injection-in-spring-app - * - mongodb-injection-in-spring-app + * - xpath-injection + * - mongodb-injection */ public class DataQueryInjectionSpringSamples { @@ -25,7 +25,7 @@ public static class UnsafeXPathController { private final org.w3c.dom.Document usersDoc = null; // simplified @GetMapping("/data-query/xpath/spring/unsafe") - @PositiveRuleSample(value = "java/security/data-query-injection.yaml", id = "xpath-injection-in-spring-app") + @PositiveRuleSample(value = "java/security/data-query-injection.yaml", id = "xpath-injection") public String unsafeXPath(@RequestParam("username") String username, @RequestParam("password") String password) throws Exception { // VULNERABLE: user data concatenated into XPath @@ -43,7 +43,7 @@ public static class SafeXPathController { private final org.w3c.dom.Document usersDoc = null; // simplified @GetMapping("/data-query/xpath/spring/safe") - @NegativeRuleSample(value = "java/security/data-query-injection.yaml", id = "xpath-injection-in-spring-app") + @NegativeRuleSample(value = "java/security/data-query-injection.yaml", id = "xpath-injection") public String safeXPath(@RequestParam("username") String username, @RequestParam("password") String password) throws Exception { if (username == null || password == null @@ -72,7 +72,7 @@ public static class UnsafeMongoController { private final com.mongodb.DB db = null; // simplified placeholder @GetMapping("/data-query/mongo/unsafe") - @PositiveRuleSample(value = "java/security/data-query-injection.yaml", id = "mongodb-injection-in-spring-app") + @PositiveRuleSample(value = "java/security/data-query-injection.yaml", id = "mongodb-injection") public String unsafeMongo(HttpServletRequest request) { String username = request.getParameter("username"); String password = request.getParameter("password"); @@ -91,7 +91,7 @@ public static class SafeMongoController { private final com.mongodb.client.MongoDatabase db = null; // simplified placeholder @GetMapping("/data-query/mongo/safe") - @NegativeRuleSample(value = "java/security/data-query-injection.yaml", id = "mongodb-injection-in-spring-app") + @NegativeRuleSample(value = "java/security/data-query-injection.yaml", id = "mongodb-injection") public String safeMongo(@RequestParam("username") String username, @RequestParam("password") String password) { com.mongodb.client.MongoCollection users = db.getCollection("users"); diff --git a/test/src/main/java/security/externalconfigurationcontrol/SqlCatalogServletSamples.java b/test/src/main/java/security/externalconfigurationcontrol/SqlCatalogServletSamples.java index 573248b..4997926 100644 --- a/test/src/main/java/security/externalconfigurationcontrol/SqlCatalogServletSamples.java +++ b/test/src/main/java/security/externalconfigurationcontrol/SqlCatalogServletSamples.java @@ -19,7 +19,7 @@ import org.seqra.sast.test.util.PositiveRuleSample; /** - * Servlet-based samples for sql-catalog-external-manipulation-in-servlet-app rule. + * Servlet-based samples for sql-catalog-external-manipulation rule. */ public class SqlCatalogServletSamples { @@ -39,7 +39,7 @@ public UnsafeCatalogServlet(DataSource dataSource) { } @Override - @PositiveRuleSample(value = "java/security/external-configuration-control.yaml", id = "sql-catalog-external-manipulation-in-servlet-app") + @PositiveRuleSample(value = "java/security/external-configuration-control.yaml", id = "sql-catalog-external-manipulation") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String catalog = request.getParameter("catalog"); @@ -82,7 +82,7 @@ public SafeCatalogServlet(DataSource dataSource) { } @Override - @NegativeRuleSample(value = "java/security/external-configuration-control.yaml", id = "sql-catalog-external-manipulation-in-servlet-app") + @NegativeRuleSample(value = "java/security/external-configuration-control.yaml", id = "sql-catalog-external-manipulation") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String tenantId = (String) request.getAttribute("tenantId"); diff --git a/test/src/main/java/security/externalconfigurationcontrol/SqlCatalogSpringSamples.java b/test/src/main/java/security/externalconfigurationcontrol/SqlCatalogSpringSamples.java index 797634c..63ed0f3 100644 --- a/test/src/main/java/security/externalconfigurationcontrol/SqlCatalogSpringSamples.java +++ b/test/src/main/java/security/externalconfigurationcontrol/SqlCatalogSpringSamples.java @@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.RestController; /** - * Spring MVC samples for sql-catalog-external-manipulation-in-spring-app rule. + * Spring MVC samples for sql-catalog-external-manipulation rule. */ public class SqlCatalogSpringSamples { @@ -36,7 +36,7 @@ public UnsafeCatalogController(DataSource dataSource) { } @GetMapping("/unsafe") - @PositiveRuleSample(value = "java/security/external-configuration-control.yaml", id = "sql-catalog-external-manipulation-in-spring-app") + @PositiveRuleSample(value = "java/security/external-configuration-control.yaml", id = "sql-catalog-external-manipulation") public List getUsers(@RequestParam String catalog, @RequestParam int id) throws SQLException { Connection conn = DataSourceUtils.getConnection(dataSource); try { @@ -72,7 +72,7 @@ public SafeCatalogController(DataSource dataSource, TenantCatalogResolver tenant } @GetMapping("/safe") - @NegativeRuleSample(value = "java/security/external-configuration-control.yaml", id = "sql-catalog-external-manipulation-in-spring-app") + @NegativeRuleSample(value = "java/security/external-configuration-control.yaml", id = "sql-catalog-external-manipulation") public List getUsers(@RequestParam int id, @AuthenticationPrincipal TenantPrincipal principal) throws SQLException { String tenantId = principal.getTenantId(); String catalog = tenantCatalogResolver.resolveCatalogForTenant(tenantId); diff --git a/test/src/main/java/security/externalconfigurationcontrol/UnsafeReflectionServletSamples.java b/test/src/main/java/security/externalconfigurationcontrol/UnsafeReflectionServletSamples.java index b0ef5dc..e793cc3 100644 --- a/test/src/main/java/security/externalconfigurationcontrol/UnsafeReflectionServletSamples.java +++ b/test/src/main/java/security/externalconfigurationcontrol/UnsafeReflectionServletSamples.java @@ -22,7 +22,7 @@ public class UnsafeReflectionServletSamples { public static class DynamicLoaderServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/external-configuration-control.yaml", id = "unsafe-reflection-in-servlet-app") + @PositiveRuleSample(value = "java/security/external-configuration-control.yaml", id = "unsafe-reflection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @@ -52,7 +52,7 @@ public static class SafeDynamicLoaderServlet extends HttpServlet { } @Override - @NegativeRuleSample(value = "java/security/external-configuration-control.yaml", id = "unsafe-reflection-in-servlet-app") + @NegativeRuleSample(value = "java/security/external-configuration-control.yaml", id = "unsafe-reflection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { diff --git a/test/src/main/java/security/externalconfigurationcontrol/UnsafeReflectionSpringSamples.java b/test/src/main/java/security/externalconfigurationcontrol/UnsafeReflectionSpringSamples.java index 1c25de8..54bea5d 100644 --- a/test/src/main/java/security/externalconfigurationcontrol/UnsafeReflectionSpringSamples.java +++ b/test/src/main/java/security/externalconfigurationcontrol/UnsafeReflectionSpringSamples.java @@ -20,7 +20,7 @@ public class UnsafeReflectionSpringSamples { public static class UnsafeReflectionController { @GetMapping("/unsafe") - @PositiveRuleSample(value = "java/security/external-configuration-control.yaml", id = "unsafe-reflection-in-spring-app") + @PositiveRuleSample(value = "java/security/external-configuration-control.yaml", id = "unsafe-reflection") public String loadClass(@RequestParam String className) throws Exception { // UNSAFE: user input directly controls Class.forName Class clazz = Class.forName(className); @@ -41,7 +41,7 @@ public static class SafeReflectionController { } @GetMapping("/safe") - @NegativeRuleSample(value = "java/security/external-configuration-control.yaml", id = "unsafe-reflection-in-spring-app") + @NegativeRuleSample(value = "java/security/external-configuration-control.yaml", id = "unsafe-reflection") public String loadClass(@RequestParam String type) throws Exception { Class clazz = ALLOWED_CLASSES.get(type); if (clazz == null) { diff --git a/test/src/main/java/security/ldap/LdapInjectionServletSamples.java b/test/src/main/java/security/ldap/LdapInjectionServletSamples.java index abaae2b..72f18e2 100644 --- a/test/src/main/java/security/ldap/LdapInjectionServletSamples.java +++ b/test/src/main/java/security/ldap/LdapInjectionServletSamples.java @@ -67,7 +67,7 @@ public LdapInjectionServletSamples(LdapInjectionService authService) { } @Override - @PositiveRuleSample(value = "java/security/ldap.yaml", id = "ldap-injection-in-servlet-app") + @PositiveRuleSample(value = "java/security/ldap.yaml", id = "ldap-injection") protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // VULNERABLE: request parameters (untrusted) flow into LDAP filter via vulnerableAuthenticate() String username = req.getParameter("username"); @@ -82,7 +82,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S @Override // TODO: restore this when conditional validators are implemented -// @NegativeRuleSample(value = "java/security/ldap.yaml", id = "ldap-injection-in-servlet-app") +// @NegativeRuleSample(value = "java/security/ldap.yaml", id = "ldap-injection") protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // SAFE: request parameters flow into safeAuthenticate(), which uses filter arguments String username = req.getParameter("username"); diff --git a/test/src/main/java/security/ldap/LdapInjectionSpringSamples.java b/test/src/main/java/security/ldap/LdapInjectionSpringSamples.java index f4780e1..a5108b3 100644 --- a/test/src/main/java/security/ldap/LdapInjectionSpringSamples.java +++ b/test/src/main/java/security/ldap/LdapInjectionSpringSamples.java @@ -61,7 +61,7 @@ public boolean safeSearch(String username) throws Exception { public class LdapInjectionSpringSamples { @RestController - @RequestMapping("/ldap-injection-in-spring-app") + @RequestMapping("/ldap-injection") public static class UnsafeLdapSpringController { private final LdapInjectionSpringService ldapService; @@ -75,7 +75,7 @@ public UnsafeLdapSpringController(LdapInjectionSpringService ldapService) { * LDAP search method which concatenates them into the LDAP filter. */ @PostMapping("/unsafe") - @PositiveRuleSample(value = "java/security/ldap.yaml", id = "ldap-injection-in-spring-app") + @PositiveRuleSample(value = "java/security/ldap.yaml", id = "ldap-injection") public boolean unsafeSearch(@RequestParam("username") String username) throws Exception { // VULNERABLE: username flows into vulnerableSearch(), which builds an LDAP filter via concatenation return ldapService.vulnerableSearch(username); @@ -83,7 +83,7 @@ public boolean unsafeSearch(@RequestParam("username") String username) throws Ex } @RestController - @RequestMapping("/ldap-injection-in-spring-app") + @RequestMapping("/ldap-injection") public static class SafeLdapSpringController { private final LdapInjectionSpringService ldapService; @@ -94,7 +94,7 @@ public SafeLdapSpringController(LdapInjectionSpringService ldapService) { @GetMapping("/safe") // TODO: restore this when conditional validators are implemented -// @NegativeRuleSample(value = "java/security/ldap.yaml", id = "ldap-injection-in-spring-app") +// @NegativeRuleSample(value = "java/security/ldap.yaml", id = "ldap-injection") public boolean safeSearch(@RequestParam("username") String username) throws Exception { return ldapService.safeSearch(username); } diff --git a/test/src/main/java/security/loginjection/LogInjectionSamples.java b/test/src/main/java/security/loginjection/LogInjectionSamples.java index 1f09ec1..14ea329 100644 --- a/test/src/main/java/security/loginjection/LogInjectionSamples.java +++ b/test/src/main/java/security/loginjection/LogInjectionSamples.java @@ -22,13 +22,13 @@ */ public class LogInjectionSamples { - // log-injection-in-servlet-app + // log-injection @WebServlet("/log-injection-in-servlet/unsafe") public static class UnsafeLogServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/log-injection.yaml", id = "log-injection-in-servlet-app") + @PositiveRuleSample(value = "java/security/log-injection.yaml", id = "log-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); // untrusted @@ -46,7 +46,7 @@ public static class SafeLogServlet extends HttpServlet { @Override // TODO: restore this when conditional validators are implemented -// @NegativeRuleSample(value = "java/security/log-injection.yaml", id = "log-injection-in-servlet-app") +// @NegativeRuleSample(value = "java/security/log-injection.yaml", id = "log-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); @@ -67,7 +67,7 @@ private static String sanitizeForLog(String value) { return value.replaceAll("[\\r\\n\\t\\x00-\\x1F]", "_"); } - // log-injection-in-spring-app + // log-injection @org.springframework.web.bind.annotation.RestController @org.springframework.web.bind.annotation.RequestMapping("/login/log-injection") @@ -76,7 +76,7 @@ public static class SpringLogInjectionController { private static final Logger logger = LoggerFactory.getLogger(SpringLogInjectionController.class); @org.springframework.web.bind.annotation.PostMapping("/unsafe") - @PositiveRuleSample(value = "java/security/log-injection.yaml", id = "log-injection-in-spring-app") + @PositiveRuleSample(value = "java/security/log-injection.yaml", id = "log-injection") public org.springframework.http.ResponseEntity unsafeLogin( @org.springframework.web.bind.annotation.RequestParam String username, @org.springframework.web.bind.annotation.RequestParam String password) { @@ -88,7 +88,7 @@ public org.springframework.http.ResponseEntity unsafeLogin( /* @org.springframework.web.bind.annotation.PostMapping("/safe") - @NegativeRuleSample(value = "java/security/log-injection.yaml", id = "log-injection-in-spring-app") + @NegativeRuleSample(value = "java/security/log-injection.yaml", id = "log-injection") public org.springframework.http.ResponseEntity safeLogin( @org.springframework.web.bind.annotation.RequestParam String username, @org.springframework.web.bind.annotation.RequestParam String password) { @@ -105,7 +105,7 @@ public org.springframework.http.ResponseEntity safeLogin( */ } - // seam-log-injection-in-servlet-app + // seam-log-injection @Name("seamLoginActionServletStyle") public static class SeamServletStyleLoginAction { diff --git a/test/src/main/java/security/pathtraversal/PathTraversalServletSamples.java b/test/src/main/java/security/pathtraversal/PathTraversalServletSamples.java index 39331a1..83b84fd 100644 --- a/test/src/main/java/security/pathtraversal/PathTraversalServletSamples.java +++ b/test/src/main/java/security/pathtraversal/PathTraversalServletSamples.java @@ -39,7 +39,7 @@ public static class UnsafeParamDownloadServlet extends HttpServlet { private static final String BASE_DIR = "/var/www/uploads/"; @Override - @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-servlet-app") + @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @@ -67,7 +67,7 @@ public static class UnsafeHeaderDownloadServlet extends HttpServlet { private static final String BASE_DIR = "/var/www/uploads/"; @Override - @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-servlet-app") + @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @@ -99,7 +99,7 @@ public static class UnsafeCookieDownloadServlet extends HttpServlet { private static final File BASE_DIR = new File("/var/www/uploads"); @Override - @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-servlet-app") + @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @@ -142,7 +142,7 @@ public static class SafeParamDownloadServlet1 extends HttpServlet { @Override // TODO: enable this test when we have conditional sanitizers -// @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-servlet-app") +// @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @@ -185,7 +185,7 @@ public static class SafeParamDownloadServlet2 extends HttpServlet { private static final File BASE_DIR = new File("/var/www/uploads").getAbsoluteFile(); @Override - @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-servlet-app") + @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @@ -218,7 +218,7 @@ public static class SafeHeaderDownloadServlet extends HttpServlet { private static final File BASE_DIR = new File("/var/www/uploads").getAbsoluteFile(); @Override - @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-servlet-app") + @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @@ -264,7 +264,7 @@ public static class UnsafeFileUploadServlet extends HttpServlet { private static final String UPLOAD_DIR = "/var/www/uploads/"; @Override - @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-servlet-app") + @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { diff --git a/test/src/main/java/security/pathtraversal/PathTraversalSpringSamples.java b/test/src/main/java/security/pathtraversal/PathTraversalSpringSamples.java index 5d7316c..9d6df33 100644 --- a/test/src/main/java/security/pathtraversal/PathTraversalSpringSamples.java +++ b/test/src/main/java/security/pathtraversal/PathTraversalSpringSamples.java @@ -40,7 +40,7 @@ public static class UnsafeFileDownloadController { * VULNERABLE: untrusted @PathVariable is concatenated directly into a path. */ @GetMapping("/unsafe/{*fileName}") - @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-spring-app") + @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") public ResponseEntity unsafePathVariableDownload(@PathVariable String fileName) { // VULNERABLE: direct concatenation of user input into path @@ -58,7 +58,7 @@ public ResponseEntity unsafePathVariableDownload(@PathVariabl * any validation or base-directory enforcement. */ @GetMapping("/unsafe-param") - @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-spring-app") + @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") public ResponseEntity unsafeParamDownload(@RequestParam("file") String fileName) { Path path = Paths.get(BASE_DIR + fileName); @@ -74,7 +74,7 @@ public ResponseEntity unsafeParamDownload(@RequestParam("file * VULNERABLE: takes a header value and concatenates it into a path. */ @GetMapping("/unsafe-header") - @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-spring-app") + @PositiveRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") public ResponseEntity unsafeHeaderDownload(@RequestHeader("X-Download-File") String headerName) { Path path = Paths.get(BASE_DIR + headerName); @@ -91,7 +91,7 @@ public ResponseEntity unsafeHeaderDownload(@RequestHeader("X- * so path traversal sequences like "../" are not possible. */ @GetMapping("/safe-pathvar/{fileName}") - @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-spring-app") + @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") public ResponseEntity safeNonWildcardPathVariable(@PathVariable String fileName) { // Without a wildcard in the URL pattern (e.g., {*fileName}), @@ -119,7 +119,7 @@ public static class SafeFileDownloadController { */ @GetMapping("/safe/{*fileName}") // TODO: restore this when conditional sanitizers are implemented -// @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-spring-app") +// @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") public ResponseEntity safePathVariableDownload(@PathVariable String fileName) { Path target = prepareValidatedTarget(fileName); @@ -133,7 +133,7 @@ public ResponseEntity safePathVariableDownload(@PathVariable */ @GetMapping("/safe-param") // TODO: restore this when conditional sanitizers are implemented -// @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-spring-app") +// @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") public ResponseEntity safeParamDownload(@RequestParam("file") String fileName) { Path target = prepareValidatedTarget(fileName); @@ -146,7 +146,7 @@ public ResponseEntity safeParamDownload(@RequestParam("file") * of filenames, avoiding direct use of untrusted path fragments. */ @GetMapping("/safe-header") - @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal-in-spring-app") + @NegativeRuleSample(value = "java/security/path-traversal.yaml", id = "path-traversal") public ResponseEntity safeHeaderDownload(@RequestHeader("X-Download-File") String headerName) { Map allowlist = new HashMap(); diff --git a/test/src/main/java/security/sqli/SqlInjectionServletSamples.java b/test/src/main/java/security/sqli/SqlInjectionServletSamples.java index 89ac2ad..7d009f4 100644 --- a/test/src/main/java/security/sqli/SqlInjectionServletSamples.java +++ b/test/src/main/java/security/sqli/SqlInjectionServletSamples.java @@ -32,7 +32,7 @@ public static class UnsafeSqlServlet extends HttpServlet { private DataSource dataSource; @Override - @PositiveRuleSample(value = "java/security/sqli.yaml", id = "sql-injection-in-servlet-app") + @PositiveRuleSample(value = "java/security/sqli.yaml", id = "sql-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userId = request.getParameter("userId"); // untrusted input @@ -63,7 +63,7 @@ public static class SafeSqlServlet extends HttpServlet { private DataSource dataSource; @Override - @NegativeRuleSample(value = "java/security/sqli.yaml", id = "sql-injection-in-servlet-app") + @NegativeRuleSample(value = "java/security/sqli.yaml", id = "sql-injection") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userId = request.getParameter("userId"); diff --git a/test/src/main/java/security/sqli/SqlInjectionSpringSamples.java b/test/src/main/java/security/sqli/SqlInjectionSpringSamples.java index 2b2893f..1b59c74 100644 --- a/test/src/main/java/security/sqli/SqlInjectionSpringSamples.java +++ b/test/src/main/java/security/sqli/SqlInjectionSpringSamples.java @@ -14,7 +14,7 @@ public class SqlInjectionSpringSamples { @RestController - @RequestMapping("/sql-injection-in-spring-app") + @RequestMapping("/sql-injection") public static class UnsafeSqlSpringController { private final JdbcTemplate jdbcTemplate; @@ -27,7 +27,7 @@ public UnsafeSqlSpringController(JdbcTemplate jdbcTemplate) { * Unsafe endpoint that concatenates untrusted request parameters into a SQL query. */ @GetMapping("/unsafe") - @PositiveRuleSample(value = "java/security/sqli.yaml", id = "sql-injection-in-spring-app") + @PositiveRuleSample(value = "java/security/sqli.yaml", id = "sql-injection") public String unsafeSearch(@RequestParam("username") String username) { // VULNERABLE: username is directly concatenated into the SQL string String sql = "SELECT id, username FROM users WHERE username = '" + username + "'"; @@ -48,7 +48,7 @@ public String unsafeSearch(@RequestParam("username") String username) { } @RestController - @RequestMapping("/sql-injection-in-spring-app") + @RequestMapping("/sql-injection") public static class SafeSqlSpringController { private final JdbcTemplate jdbcTemplate; @@ -61,7 +61,7 @@ public SafeSqlSpringController(JdbcTemplate jdbcTemplate) { * Safe endpoint that uses parameterized queries and basic validation. */ @GetMapping("/safe") - @NegativeRuleSample(value = "java/security/sqli.yaml", id = "sql-injection-in-spring-app") + @NegativeRuleSample(value = "java/security/sqli.yaml", id = "sql-injection") public String safeSearch(@RequestParam("username") String username) { if (username == null || username.isBlank()) { return ""; // simple guard; in a real app, you might return 400 or an error body diff --git a/test/src/main/java/security/ssrf/SsrfSamples.java b/test/src/main/java/security/ssrf/SsrfSamples.java index bda7d07..d463d57 100644 --- a/test/src/main/java/security/ssrf/SsrfSamples.java +++ b/test/src/main/java/security/ssrf/SsrfSamples.java @@ -37,13 +37,13 @@ */ public class SsrfSamples { - // ssrf-in-servlet-app + // ssrf @WebServlet("/ssrf/unsafe-proxy") public static class UnsafeProxyServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/ssrf.yaml", id = "ssrf-in-servlet-app") + @PositiveRuleSample(value = "java/security/ssrf.yaml", id = "ssrf") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // User controls full target URL @@ -79,7 +79,7 @@ public static class SafeProxyServlet extends HttpServlet { ); @Override - @NegativeRuleSample(value = "java/security/ssrf.yaml", id = "ssrf-in-servlet-app") + @NegativeRuleSample(value = "java/security/ssrf.yaml", id = "ssrf") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String targetUrl = request.getParameter("url"); @@ -140,7 +140,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) } } - // ssrf-in-spring-app + // ssrf @RestController @RequestMapping("/ssrf/proxy") @@ -149,7 +149,7 @@ public static class SsrfSpringController { private final RestTemplate restTemplate = new RestTemplate(); @GetMapping("/unsafe") - @PositiveRuleSample(value = "java/security/ssrf.yaml", id = "ssrf-in-spring-app") + @PositiveRuleSample(value = "java/security/ssrf.yaml", id = "ssrf") public ResponseEntity unsafeProxy(@RequestParam("url") String targetUrl) { if (targetUrl == null || targetUrl.isBlank()) { return ResponseEntity.badRequest().body("Missing 'url' parameter"); @@ -167,7 +167,7 @@ public ResponseEntity unsafeProxy(@RequestParam("url") String targetUrl) @GetMapping("/safe") // TODO: restore this when conditional validators are implemented -// @NegativeRuleSample(value = "java/security/ssrf.yaml", id = "ssrf-in-spring-app") +// @NegativeRuleSample(value = "java/security/ssrf.yaml", id = "ssrf") public ResponseEntity safeProxy(@RequestParam("url") String targetUrl) { if (targetUrl == null || targetUrl.isBlank()) { return ResponseEntity.badRequest().body("Missing 'url' parameter"); diff --git a/test/src/main/java/security/strings/StringsSamples.java b/test/src/main/java/security/strings/StringsSamples.java index 1d706dd..91d6598 100644 --- a/test/src/main/java/security/strings/StringsSamples.java +++ b/test/src/main/java/security/strings/StringsSamples.java @@ -52,13 +52,13 @@ public String normalizeBeforeValidation(String input) throws Exception { return userInput; } - // format-string-external-manipulation-in-servlet-app (join rule via servlet untrusted source) + // format-string-external-manipulation (join rule via servlet untrusted source) @WebServlet("/strings/format/servlet") public static class FormatStringServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/strings.yaml", id = "format-string-external-manipulation-in-servlet-app") + @PositiveRuleSample(value = "java/security/strings.yaml", id = "format-string-external-manipulation") protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userFormat = request.getParameter("fmt"); String value = request.getParameter("value"); @@ -70,7 +70,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t writer.println(formatted); } - @NegativeRuleSample(value = "java/security/strings.yaml", id = "format-string-external-manipulation-in-servlet-app") + @NegativeRuleSample(value = "java/security/strings.yaml", id = "format-string-external-manipulation") protected void doGetSafe(HttpServletRequest request, HttpServletResponse response) throws IOException { String value = request.getParameter("value"); @@ -82,7 +82,7 @@ protected void doGetSafe(HttpServletRequest request, HttpServletResponse respons } } - // format-string-external-manipulation-in-spring-app (join rule via Spring untrusted source) + // format-string-external-manipulation (join rule via Spring untrusted source) @Controller @RequestMapping("/strings/format") @@ -90,7 +90,7 @@ public static class FormatStringSpringController { @GetMapping("/unsafe") @ResponseBody - @PositiveRuleSample(value = "java/security/strings.yaml", id = "format-string-external-manipulation-in-spring-app") + @PositiveRuleSample(value = "java/security/strings.yaml", id = "format-string-external-manipulation") public String unsafe(@RequestParam("fmt") String fmt, @RequestParam("value") String value) { // VULNERABLE: user-controlled format string @@ -99,7 +99,7 @@ public String unsafe(@RequestParam("fmt") String fmt, @GetMapping("/safe") @ResponseBody - @NegativeRuleSample(value = "java/security/strings.yaml", id = "format-string-external-manipulation-in-spring-app") + @NegativeRuleSample(value = "java/security/strings.yaml", id = "format-string-external-manipulation") public String safe(@RequestParam("value") String value) { // SAFE: use a hardcoded format string, user input as data only return String.format("Value: %s", value); diff --git a/test/src/main/java/security/unsafedeserialization/UnsafeDeserializationSamples.java b/test/src/main/java/security/unsafedeserialization/UnsafeDeserializationSamples.java index 02196bb..05d452e 100644 --- a/test/src/main/java/security/unsafedeserialization/UnsafeDeserializationSamples.java +++ b/test/src/main/java/security/unsafedeserialization/UnsafeDeserializationSamples.java @@ -39,13 +39,13 @@ */ public class UnsafeDeserializationSamples { - // unsafe-object-mapper-in-servlet-app + // unsafe-object-mapper @WebServlet("/deserialize/unsafe-object-input-stream") public static class UnsafeObjectInputStreamServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-object-mapper-in-servlet-app") + @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-object-mapper") protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // VULNERABLE: directly deserialize from request input stream try (ObjectInputStream ois = new ObjectInputStream(req.getInputStream())) { @@ -61,7 +61,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S public static class SafeObjectInputStreamServlet extends HttpServlet { @Override - @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-object-mapper-in-servlet-app") + @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-object-mapper") protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try (ObjectInputStream ois = new ObjectInputStream(new java.io.FileInputStream("/tmp/last_resource"))) { Object obj = ois.readObject(); @@ -72,14 +72,14 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S } } - // unsafe-object-mapper-in-spring-app + // unsafe-object-mapper @RestController @RequestMapping("/api/deserialize/object-input-stream") public static class ObjectInputStreamSpringController { @PostMapping(path = "/unsafe", consumes = org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE) - @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-object-mapper-in-spring-app") + @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-object-mapper") public ResponseEntity unsafeDeserialize(@RequestBody byte[] body) { try (ObjectInputStream ois = new ObjectInputStream(new java.io.ByteArrayInputStream(body))) { Object obj = ois.readObject(); @@ -90,7 +90,7 @@ public ResponseEntity unsafeDeserialize(@RequestBody byte[] body) { } @PostMapping(path = "/safe", consumes = org.springframework.http.MediaType.APPLICATION_JSON_VALUE) - @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-object-mapper-in-spring-app") + @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-object-mapper") public ResponseEntity safeDeserialize(@RequestBody SafeDto dto) { // SAFE: rely on Spring's JSON binding into a constrained DTO type if (dto == null || dto.name == null || dto.name.length() > 100) { @@ -148,10 +148,10 @@ public void onMessage(Message message) { } } - // unsafe-jackson-deserialization-in-servlet-app + // unsafe-jackson-deserialization @WebServlet("/deserialize/jackson/unsafe") - @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-jackson-deserialization-in-servlet-app") + @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-jackson-deserialization") public static class UnsafeJacksonServlet extends HttpServlet { // VULNERABLE: default typing enabled globally -> potential RCE gadget exploitation @@ -166,7 +166,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S } @WebServlet("/deserialize/jackson/safe") - @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-jackson-deserialization-in-servlet-app") + @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-jackson-deserialization") public static class SafeJacksonServlet extends HttpServlet { private final ObjectMapper mapper; @@ -188,11 +188,11 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S } } - // unsafe-jackson-deserialization-in-spring-app + // unsafe-jackson-deserialization @RestController @RequestMapping("/api/deserialize/jackson") - @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-jackson-deserialization-in-spring-app") + @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-jackson-deserialization") public static class JacksonSpringController { // VULNERABLE: default typing enabled globally -> potential RCE gadget exploitation @@ -206,7 +206,7 @@ public ResponseEntity unsafeJackson(@RequestBody String json) throws IOE } @PostMapping(path = "/safe", consumes = org.springframework.http.MediaType.APPLICATION_JSON_VALUE) - @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-jackson-deserialization-in-spring-app") + @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-jackson-deserialization") public ResponseEntity safeJackson(@RequestBody SafeDto dto) { if (dto == null || dto.name == null || dto.name.isBlank()) { return ResponseEntity.badRequest().build(); @@ -241,13 +241,13 @@ public interface SafeRemoteService extends Remote { String invokeById(long commandId) throws RemoteException; } - // java-servlet-unsafe-snake-yaml-deserialization / spring-unsafe-snake-yaml-deserialization + // unsafe-snake-yaml-deserialization / spring-unsafe-snake-yaml-deserialization @WebServlet("/yaml/unsafe") public static class UnsafeSnakeYamlServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "java-servlet-unsafe-snake-yaml-deserialization") + @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-snake-yaml-deserialization") protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // VULNERABLE: use default SnakeYAML constructor on user-provided data Yaml yaml = new Yaml(); @@ -260,7 +260,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S public static class SafeSnakeYamlServlet extends HttpServlet { @Override - @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "java-servlet-unsafe-snake-yaml-deserialization") + @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-snake-yaml-deserialization") protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // SAFE-ish: treat YAML as plain text or use a safe subset parser (simulated here) String body = new String(req.getInputStream().readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); @@ -277,7 +277,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S public static class SnakeYamlSpringController { @PostMapping(path = "/unsafe", consumes = "application/x-yaml") - @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "spring-unsafe-snake-yaml-deserialization") + @PositiveRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-snake-yaml-deserialization") public ResponseEntity unsafeYaml(@RequestBody byte[] yamlBytes) { Yaml yaml = new Yaml(); Object obj = yaml.load(new java.io.ByteArrayInputStream(yamlBytes)); @@ -285,7 +285,7 @@ public ResponseEntity unsafeYaml(@RequestBody byte[] yamlBytes) { } @PostMapping(path = "/safe", consumes = "application/x-yaml") - @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "spring-unsafe-snake-yaml-deserialization") + @NegativeRuleSample(value = "java/security/unsafe-deserialization.yaml", id = "unsafe-snake-yaml-deserialization") public ResponseEntity safeYaml(@RequestBody String yamlText) { if (yamlText.length() > 4096) { return ResponseEntity.badRequest().body("YAML too large"); diff --git a/test/src/main/java/security/xxe/XxeSamples.java b/test/src/main/java/security/xxe/XxeSamples.java index 368c5c1..330c91f 100644 --- a/test/src/main/java/security/xxe/XxeSamples.java +++ b/test/src/main/java/security/xxe/XxeSamples.java @@ -29,13 +29,11 @@ */ public class XxeSamples { - // xxe-in-servlet-app - @WebServlet("/xxe/upload") public static class UnsafeXmlUploadServlet extends HttpServlet { @Override - @PositiveRuleSample(value = "java/security/xxe.yaml", id = "xxe-in-servlet-app") + @PositiveRuleSample(value = "java/security/xxe.yaml", id = "xxe") protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { @@ -59,7 +57,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) public static class SafeXmlUploadServlet extends HttpServlet { @Override - @NegativeRuleSample(value = "java/security/xxe.yaml", id = "xxe-in-servlet-app") + @NegativeRuleSample(value = "java/security/xxe.yaml", id = "xxe") protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { @@ -98,14 +96,14 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) } } - // xxe-in-spring-app + // xxe @RestController @RequestMapping("/api/xxe") public static class XxeSpringController { @PostMapping(value = "/process-xml", consumes = MediaType.APPLICATION_XML_VALUE) - @PositiveRuleSample(value = "java/security/xxe.yaml", id = "xxe-in-spring-app") + @PositiveRuleSample(value = "java/security/xxe.yaml", id = "xxe") public ResponseEntity processXmlInsecure(@RequestBody String xml) throws Exception { // Insecure: default configuration may allow DTDs and external entities DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); @@ -117,7 +115,7 @@ public ResponseEntity processXmlInsecure(@RequestBody String xml) throws } @PostMapping(value = "/process-xml-safe", consumes = MediaType.APPLICATION_XML_VALUE) - @NegativeRuleSample(value = "java/security/xxe.yaml", id = "xxe-in-spring-app") + @NegativeRuleSample(value = "java/security/xxe.yaml", id = "xxe") public ResponseEntity processXmlSafe(@RequestBody String xml) throws Exception { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();