diff --git a/.github/workflows/ci-analyzer-owasp.yaml b/.github/workflows/ci-analyzer-owasp.yaml index 40a63024a..0d1b424d0 100644 --- a/.github/workflows/ci-analyzer-owasp.yaml +++ b/.github/workflows/ci-analyzer-owasp.yaml @@ -21,7 +21,7 @@ concurrency: cancel-in-progress: true env: - EXPECTED_TRACES: 3011 + EXPECTED_TRACES: 3835 jobs: owasp: diff --git a/rules/README.md b/rules/README.md index 2050a8ff3..6efed4230 100644 --- a/rules/README.md +++ b/rules/README.md @@ -59,11 +59,11 @@ lib/ command-injection-sinks.yaml servlet-sqli-sinks.yaml servlet-untrusted-data-source.yaml - servlet-xss-sinks.yaml + servlet-response-injection-sinks.yaml xxe-sinks.yaml spring/ jdbc-sqli-sinks.yaml - spring-xss-sinks.yaml + spring-response-injection-sinks.yaml untrusted-data-source.yaml ``` diff --git a/rules/ruleset/java/lib/generic/servlet-response-injection-sinks.yaml b/rules/ruleset/java/lib/generic/servlet-response-injection-sinks.yaml new file mode 100644 index 000000000..3e4689394 --- /dev/null +++ b/rules/ruleset/java/lib/generic/servlet-response-injection-sinks.yaml @@ -0,0 +1,58 @@ +rules: + - id: java-servlet-response-injection-sink + options: + lib: true + severity: NOTE + message: Direct write of unvalidated user input into response + metadata: + provenance: https://github.com/semgrep/semgrep-rules/blob/develop/java/lang/security/audit/xss/no-direct-response-writer.yaml + languages: + - java + mode: taint + pattern-sanitizers: + - patterns: + - pattern-either: + - pattern: Encode.forHtml(..., $UNTRUSTED, ...) + - pattern: (PolicyFactory $POLICY).sanitize(..., $UNTRUSTED, ...) + - pattern: (AntiSamy $AS).scan(..., $UNTRUSTED, ...) + - pattern: JSoup.clean(..., $UNTRUSTED, ...) + - pattern: HtmlUtils.htmlEscape(..., $UNTRUSTED, ...) + - pattern: org.apache.commons.lang.StringEscapeUtils.escapeHtml(..., $UNTRUSTED, ...) + - pattern: org.apache.commons.text.StringEscapeUtils.escapeHtml3(..., $UNTRUSTED, ...) + - pattern: org.apache.commons.text.StringEscapeUtils.escapeHtml4(..., $UNTRUSTED, ...) + - pattern: org.owasp.esapi.ESAPI.encoder().encodeForHTML(..., $UNTRUSTED, ...) + - focus-metavariable: $UNTRUSTED + + pattern-sinks: + - patterns: + - pattern-either: + - patterns: + - patterns: + - pattern-inside: | + $RETURNTYPE $ENTRYPOINT(HttpServletRequest $_, HttpServletResponse $RESPONSE) { + ... + } + - metavariable-pattern: + metavariable: $ENTRYPOINT + pattern-either: + - pattern: doDelete + - pattern: doGet + - pattern: doPost + - pattern: doPut + - pattern: doTrace + - pattern: _jspService + - patterns: + - pattern-inside: | + $W = (HttpServletResponse $RESPONSE).getWriter(...); + ... + - pattern: | + $W.$WRITE(..., $UNTRUSTED, ...); + - patterns: + - pattern-inside: | + $S = (HttpServletResponse $RESPONSE).getOutputStream(...); + ... + - pattern: | + $S.$WRITE(..., $UNTRUSTED, ...); + - pattern: (HttpServletResponse $RESPONSE).sendError($CODE, $UNTRUSTED) + - pattern: (JspWriter $W).$WRITE(..., $UNTRUSTED, ...) + - focus-metavariable: $UNTRUSTED diff --git a/rules/ruleset/java/lib/generic/servlet-xss-html-response-sinks.yaml b/rules/ruleset/java/lib/generic/servlet-xss-html-response-sinks.yaml new file mode 100644 index 000000000..127d188a3 --- /dev/null +++ b/rules/ruleset/java/lib/generic/servlet-xss-html-response-sinks.yaml @@ -0,0 +1,78 @@ +rules: + - id: java-servlet-xss-html-response-sink + options: + lib: true + severity: NOTE + message: Direct write of unvalidated user input into a response without safe content type + metadata: + provenance: + - https://github.com/github/codeql/blob/main/java/ql/lib/semmle/code/java/security/XSS.qll + languages: + - java + mode: taint + + + pattern-sanitizers: + - patterns: + - pattern-either: + - pattern: Encode.forHtml(..., $UNTRUSTED, ...) + - pattern: (PolicyFactory $POLICY).sanitize(..., $UNTRUSTED, ...) + - pattern: (AntiSamy $AS).scan(..., $UNTRUSTED, ...) + - pattern: JSoup.clean(..., $UNTRUSTED, ...) + - pattern: HtmlUtils.htmlEscape(..., $UNTRUSTED, ...) + - pattern: org.apache.commons.lang.StringEscapeUtils.escapeHtml(..., $UNTRUSTED, ...) + - pattern: org.apache.commons.text.StringEscapeUtils.escapeHtml3(..., $UNTRUSTED, ...) + - pattern: org.apache.commons.text.StringEscapeUtils.escapeHtml4(..., $UNTRUSTED, ...) + - pattern: org.owasp.esapi.ESAPI.encoder().encodeForHTML(..., $UNTRUSTED, ...) + - focus-metavariable: $UNTRUSTED + + pattern-sinks: + - patterns: + - pattern-inside: | + $RETURNTYPE $ENTRYPOINT(HttpServletRequest $_, HttpServletResponse $RESPONSE) { + ... + } + - metavariable-pattern: + metavariable: $ENTRYPOINT + pattern-either: + - pattern: doDelete + - pattern: doGet + - pattern: doPost + - pattern: doPut + - pattern: doTrace + - pattern: _jspService + - pattern-either: + - pattern: | + $W = (HttpServletResponse $RESPONSE).getWriter(...); + ... + $W.$WRITE(..., $UNTRUSTED, ...); + - pattern: | + $S = (HttpServletResponse $RESPONSE).getOutputStream(...); + ... + $S.$WRITE(..., $UNTRUSTED, ...); + - pattern: (HttpServletResponse $RESPONSE).sendError($CODE, $UNTRUSTED) + - pattern: (JspWriter $W).$WRITE(..., $UNTRUSTED, ...) + + - pattern-not-inside: | + (HttpServletResponse $RESPONSE).setContentType("$CT_SAFE"); + ... + - metavariable-regex: + metavariable: $CT_SAFE + regex: '^(application/(json|pdf|octet-stream|xml)|text/(plain|xml)|image/(png|jpeg|gif))(\s*;.*)?$' + - pattern-not-inside: | + (HttpServletResponse $RESPONSE).setContentType($CT_CONST); + ... + - metavariable-pattern: + metavariable: $CT_CONST + patterns: + - pattern-either: + - pattern: MediaType.APPLICATION_JSON_VALUE + - pattern: MediaType.TEXT_PLAIN_VALUE + - pattern: MediaType.APPLICATION_PDF_VALUE + - pattern: MediaType.APPLICATION_OCTET_STREAM_VALUE + - pattern: MediaType.APPLICATION_XML_VALUE + - pattern: MediaType.TEXT_XML_VALUE + - pattern: MediaType.IMAGE_PNG_VALUE + - pattern: MediaType.IMAGE_JPEG_VALUE + - pattern: MediaType.IMAGE_GIF_VALUE + - focus-metavariable: $UNTRUSTED diff --git a/rules/ruleset/java/lib/generic/servlet-xss-sinks.yaml b/rules/ruleset/java/lib/generic/servlet-xss-sinks.yaml deleted file mode 100644 index f27ac3b02..000000000 --- a/rules/ruleset/java/lib/generic/servlet-xss-sinks.yaml +++ /dev/null @@ -1,43 +0,0 @@ -rules: - - id: java-servlet-xss-sink - options: - lib: true - severity: NOTE - message: Direct write of unvalidated user input into response - metadata: - provenance: https://github.com/semgrep/semgrep-rules/blob/develop/java/lang/security/audit/xss/no-direct-response-writer.yaml - languages: - - java - mode: taint - pattern-sanitizers: - - pattern-either: - - pattern: Encode.forHtml(...) - - pattern: (PolicyFactory $POLICY).sanitize(...) - - pattern: (AntiSamy $AS).scan(...) - - pattern: JSoup.clean(...) - - pattern: HtmlUtils.htmlEscape(...) - - pattern: org.apache.commons.lang.StringEscapeUtils.escapeHtml(...) - - pattern: org.apache.commons.text.StringEscapeUtils.escapeHtml3(...) - - pattern: org.apache.commons.text.StringEscapeUtils.escapeHtml4(...) - - pattern: org.owasp.esapi.ESAPI.encoder().encodeForHTML(...) - - pattern-sinks: - - patterns: - - pattern-either: - - pattern: | - (HttpServletResponse $RESPONSE).getWriter(...).$WRITE(..., $UNTRUSTED, ...) - - pattern: | - (HttpServletResponse $RESPONSE).getOutputStream(...).$WRITE(..., $UNTRUSTED, ...) - - pattern: | - (HttpServletResponse $RESPONSE).sendError($CODE, $UNTRUSTED) - - pattern: | - (java.io.PrintWriter $WRITER).$WRITE(..., $UNTRUSTED, ...) - - pattern: | - (PrintWriter $WRITER).$WRITE(..., $UNTRUSTED, ...) - - pattern: | - (javax.servlet.ServletOutputStream $WRITER).$WRITE(..., $UNTRUSTED, ...) - - pattern: | - (ServletOutputStream $WRITER).$WRITE(..., $UNTRUSTED, ...) - - pattern: | - (jakarta.servlet.jsp.JspWriter $WRITER).$WRITE(..., $UNTRUSTED, ...) - - focus-metavariable: $UNTRUSTED diff --git a/rules/ruleset/java/lib/spring/spring-xss-sinks.yaml b/rules/ruleset/java/lib/spring/spring-response-injection-sinks.yaml similarity index 95% rename from rules/ruleset/java/lib/spring/spring-xss-sinks.yaml rename to rules/ruleset/java/lib/spring/spring-response-injection-sinks.yaml index df8b223a3..792a02329 100644 --- a/rules/ruleset/java/lib/spring/spring-xss-sinks.yaml +++ b/rules/ruleset/java/lib/spring/spring-response-injection-sinks.yaml @@ -1,5 +1,5 @@ rules: - - id: spring-xss-sink + - id: spring-response-injection-sink options: lib: true severity: NOTE @@ -41,7 +41,6 @@ rules: - pattern: PatchMapping - pattern: PostMapping - pattern: PutMapping - - pattern: (HttpServletResponse $RESPONSE).sendError($CODE, $UNTRUSTED) - pattern: | (HttpServletResponse $RESPONSE).getWriter(...).$WRITE(..., $UNTRUSTED, ...) - pattern: | diff --git a/rules/ruleset/java/lib/spring/spring-xss-html-response-sinks.yaml b/rules/ruleset/java/lib/spring/spring-xss-html-response-sinks.yaml new file mode 100644 index 000000000..66b69217d --- /dev/null +++ b/rules/ruleset/java/lib/spring/spring-xss-html-response-sinks.yaml @@ -0,0 +1,508 @@ +rules: + - id: spring-xss-html-response-sink + options: + lib: true + severity: NOTE + message: Return of unvalidated user input from a Spring handler vulnerable to XSS + metadata: + provenance: + - https://github.com/github/codeql/blob/main/java/ql/lib/semmle/code/java/frameworks/spring/SpringHttp.qll + languages: + - java + mode: taint + + + pattern-sanitizers: + - patterns: + - pattern-either: + - pattern: Encode.forHtml(..., $UNTRUSTED, ...) + - pattern: (PolicyFactory $POLICY).sanitize(..., $UNTRUSTED, ...) + - pattern: (AntiSamy $AS).scan(..., $UNTRUSTED, ...) + - pattern: JSoup.clean(..., $UNTRUSTED, ...) + - pattern: HtmlUtils.htmlEscape(..., $UNTRUSTED, ...) + - pattern: org.apache.commons.lang.StringEscapeUtils.escapeHtml(..., $UNTRUSTED, ...) + - pattern: org.apache.commons.text.StringEscapeUtils.escapeHtml3(..., $UNTRUSTED, ...) + - pattern: org.apache.commons.text.StringEscapeUtils.escapeHtml4(..., $UNTRUSTED, ...) + - pattern: org.owasp.esapi.ESAPI.encoder().encodeForHTML(..., $UNTRUSTED, ...) + - focus-metavariable: $UNTRUSTED + + + - patterns: + - pattern-either: + - pattern: | + $H.setContentType(MediaType.APPLICATION_JSON); + ... + new ResponseEntity($UNTRUSTED, $H, ...); + - pattern: | + $H.setContentType(MediaType.APPLICATION_PDF); + ... + new ResponseEntity($UNTRUSTED, $H, ...); + - pattern: | + $H.setContentType(MediaType.APPLICATION_OCTET_STREAM); + ... + new ResponseEntity($UNTRUSTED, $H, ...); + - pattern: | + $H.setContentType(MediaType.TEXT_PLAIN); + ... + new ResponseEntity($UNTRUSTED, $H, ...); + - pattern: | + $H.setContentType(MediaType.APPLICATION_XML); + ... + new ResponseEntity($UNTRUSTED, $H, ...); + - focus-metavariable: $UNTRUSTED + + pattern-sinks: + - patterns: + - metavariable-pattern: + metavariable: $ANNOTATION + patterns: + - pattern-either: + - pattern: GetMapping + - pattern: PostMapping + - pattern: PutMapping + - pattern: PatchMapping + - pattern: DeleteMapping + - pattern: RequestMapping + - pattern-either: + - patterns: + - pattern-either: + - pattern: | + return ResponseEntity.ok($UNTRUSTED); + - patterns: + - patterns: + - pattern-not-inside: | + $X.contentType(MediaType.APPLICATION_JSON); + ... + - pattern-not-inside: | + $X.contentType(MediaType.APPLICATION_PDF); + ... + - pattern-not-inside: | + $X.contentType(MediaType.APPLICATION_OCTET_STREAM); + ... + - pattern-not-inside: | + $X.contentType(MediaType.TEXT_PLAIN); + ... + - pattern-not-inside: | + $X.contentType(MediaType.APPLICATION_XML); + ... + - pattern-not-inside: | + $X.contentType(MediaType.IMAGE_PNG); + ... + - pattern-not-inside: | + $X.contentType(MediaType.IMAGE_JPEG); + ... + - pattern-not-inside: | + $X.contentType(MediaType.IMAGE_GIF); + ... + - pattern-not-inside: | + $X.header("Content-Type", "application/json"); + ... + - pattern-not-inside: | + $X.header("Content-Type", "application/pdf"); + ... + - pattern-not-inside: | + $X.header("Content-Type", "application/octet-stream"); + ... + - pattern-not-inside: | + $X.header("Content-Type", "text/plain"); + ... + - pattern-not-inside: | + $X.header("Content-Type", "application/xml"); + ... + - pattern-not-inside: | + $X.header("Content-Type", "image/png"); + ... + - pattern-not-inside: | + $X.header("Content-Type", "image/jpeg"); + ... + - pattern-not-inside: | + $X.header("Content-Type", "image/gif"); + ... + - pattern-not-inside: | + $X.header("Content-Type", MediaType.APPLICATION_JSON_VALUE); + ... + - pattern-not-inside: | + $X.header("Content-Type", MediaType.APPLICATION_PDF_VALUE); + ... + - pattern-not-inside: | + $X.header("Content-Type", MediaType.APPLICATION_OCTET_STREAM_VALUE); + ... + - pattern-not-inside: | + $X.header("Content-Type", MediaType.TEXT_PLAIN_VALUE); + ... + - pattern-not-inside: | + $X.header("Content-Type", MediaType.APPLICATION_XML_VALUE); + ... + - pattern-not-inside: | + $X.header("Content-Type", MediaType.IMAGE_PNG_VALUE); + ... + - pattern-not-inside: | + $X.header("Content-Type", MediaType.IMAGE_JPEG_VALUE); + ... + - pattern-not-inside: | + $X.header("Content-Type", MediaType.IMAGE_GIF_VALUE); + ... + - pattern-either: + - pattern-inside: | + $X = ResponseEntity.status(...); + ... + - pattern-inside: | + $X = ResponseEntity.ok(); + ... + - pattern-inside: | + $X = ResponseEntity.internalServerError(); + ... + - pattern-inside: | + $X = ResponseEntity.accepted(); + ... + - pattern-inside: | + $X = ResponseEntity.badRequest(); + ... + - pattern-inside: | + $X = ResponseEntity.created(...); + ... + - pattern-inside: | + $X = ResponseEntity.unprocessableContent(); + ... + - pattern-inside: | + $X = ResponseEntity.unprocessableEntity(); + ... + - pattern: | + return $X.body($UNTRUSTED); + + - patterns: + - patterns: + - pattern-not-inside: | + $H.setContentType(MediaType.APPLICATION_JSON); + ... + - pattern-not-inside: | + $H.setContentType(MediaType.APPLICATION_PDF); + ... + - pattern-not-inside: | + $H.setContentType(MediaType.APPLICATION_OCTET_STREAM); + ... + - pattern-not-inside: | + $H.setContentType(MediaType.TEXT_PLAIN); + ... + - pattern-not-inside: | + $H.setContentType(MediaType.APPLICATION_XML); + ... + - pattern-not-inside: | + $H.setContentType(MediaType.IMAGE_PNG); + ... + - pattern-not-inside: | + $H.setContentType(MediaType.IMAGE_JPEG); + ... + - pattern-not-inside: | + $H.setContentType(MediaType.IMAGE_GIF); + ... + - pattern-either: + - pattern-inside: | + $H = new HttpHeaders(...); + ... + - pattern: | + return new ResponseEntity($UNTRUSTED, $H, ...); + - pattern-either: + - pattern-inside: | + @$ANNOTATION(...) + ResponseEntity $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + ResponseEntity $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + ResponseEntity $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + ResponseEntity $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + ResponseEntity $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + ResponseEntity $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + ResponseEntity $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + ResponseEntity $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + ResponseEntity $METHOD(...) { + ... + } + - patterns: + - pattern-either: + + - pattern-inside: | + @$ANNOTATION(...) + String $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + byte[] $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + Resource $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + InputStreamResource $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + ByteArrayResource $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + ClassPathResource $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + FileSystemResource $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + UrlResource $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + CompletableFuture $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + CompletableFuture $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + CompletableFuture $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + CompletableFuture $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + CompletableFuture $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + CompletableFuture $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + CompletableFuture $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + CompletableFuture $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + CompletableFuture $METHOD(...) { + ... + } + - pattern: return $UNTRUSTED; + - patterns: + - pattern-either: + - pattern-inside: | + @$ANNOTATION(...) + DeferredResult $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + DeferredResult $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + DeferredResult $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + DeferredResult $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + DeferredResult $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + DeferredResult $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + DeferredResult $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + DeferredResult $METHOD(...) { + ... + } + - pattern-inside: | + @$ANNOTATION(...) + DeferredResult $METHOD(...) { + ... + } + - pattern: $DR.setResult(..., $UNTRUSTED, ...); + + + - patterns: + - pattern-not-inside: | + @$ANNOTATION(..., produces = $PRODUCES, ...) + $RETURNTYPE $METHOD(...) { ... } + - metavariable-pattern: + metavariable: $PRODUCES + patterns: + - pattern-either: + - pattern: '"application/json"' + - pattern: '"text/plain"' + - pattern: '"application/pdf"' + - pattern: '"application/octet-stream"' + - pattern: '"application/xml"' + - pattern: '"text/xml"' + - pattern: '"image/png"' + - pattern: '"image/jpeg"' + - pattern: '"image/gif"' + - pattern: MediaType.APPLICATION_JSON_VALUE + - pattern: MediaType.TEXT_PLAIN_VALUE + - pattern: MediaType.APPLICATION_PDF_VALUE + - pattern: MediaType.APPLICATION_OCTET_STREAM_VALUE + - pattern: MediaType.APPLICATION_XML_VALUE + - pattern: MediaType.TEXT_XML_VALUE + - pattern: MediaType.IMAGE_PNG_VALUE + - pattern: MediaType.IMAGE_JPEG_VALUE + - pattern: MediaType.IMAGE_GIF_VALUE + - patterns: + - pattern-not-inside: | + @RequestMapping(..., produces = $CPRODUCES, ...) + class $CLAZZ { + ... + } + - metavariable-pattern: + metavariable: $CPRODUCES + patterns: + - pattern-either: + - pattern: '"application/json"' + - pattern: '"text/plain"' + - pattern: '"application/pdf"' + - pattern: '"application/octet-stream"' + - pattern: '"application/xml"' + - pattern: '"text/xml"' + - pattern: '"image/png"' + - pattern: '"image/jpeg"' + - pattern: '"image/gif"' + - pattern: MediaType.APPLICATION_JSON_VALUE + - pattern: MediaType.TEXT_PLAIN_VALUE + - pattern: MediaType.APPLICATION_PDF_VALUE + - pattern: MediaType.APPLICATION_OCTET_STREAM_VALUE + - pattern: MediaType.APPLICATION_XML_VALUE + - pattern: MediaType.TEXT_XML_VALUE + - pattern: MediaType.IMAGE_PNG_VALUE + - pattern: MediaType.IMAGE_JPEG_VALUE + - pattern: MediaType.IMAGE_GIF_VALUE + - focus-metavariable: $UNTRUSTED + + - patterns: + - pattern-inside: | + @$ANNOTATION(..., produces = $PRODUCES_HTML, ...) + $RT $METHOD(...) { ... } + - metavariable-pattern: + metavariable: $PRODUCES_HTML + patterns: + - pattern-either: + - pattern: '"text/html"' + - pattern: '"text/html;charset=UTF-8"' + - pattern: '"text/html;charset=utf-8"' + - pattern: '"image/svg+xml"' + - pattern: MediaType.TEXT_HTML_VALUE + - pattern: MediaType.IMAGE_SVG_XML_VALUE + - pattern: return $UNTRUSTED; + - focus-metavariable: $UNTRUSTED + + - patterns: + - metavariable-pattern: + metavariable: $ANNOTATION + patterns: + - pattern-either: + - pattern: GetMapping + - pattern: PostMapping + - pattern: PutMapping + - pattern: PatchMapping + - pattern: DeleteMapping + - pattern: RequestMapping + - pattern-inside: | + @$ANNOTATION(...) + $RETURNTYPE $METHOD(...) { + ... + } + - pattern-either: + - pattern: return $X.contentType(MediaType.TEXT_HTML).body($UNTRUSTED); + - pattern: return $X.contentType(MediaType.IMAGE_SVG_XML).body($UNTRUSTED); + - focus-metavariable: $UNTRUSTED + + + - patterns: + - pattern-either: + - pattern: | + (HttpServletResponse $R).setContentType("$CT_HTML"); + ... + $W = (HttpServletResponse $R).getWriter(...); + ... + $W.$WRITE(..., $UNTRUSTED, ...); + - pattern: | + (HttpServletResponse $R).setHeader("Content-Type", "$CT_HTML"); + ... + $W = (HttpServletResponse $R).getWriter(...); + ... + $W.$WRITE(..., $UNTRUSTED, ...); + - pattern: | + (HttpServletResponse $R).addHeader("Content-Type", "$CT_HTML"); + ... + $W = (HttpServletResponse $R).getWriter(...); + ... + $W.$WRITE(..., $UNTRUSTED, ...); + - metavariable-regex: + metavariable: $CT_HTML + regex: '^text/html(\s*;.*)?$' + - focus-metavariable: $UNTRUSTED diff --git a/rules/ruleset/java/security/xss.yaml b/rules/ruleset/java/security/xss.yaml index f4466a91a..cc6c0b9c8 100644 --- a/rules/ruleset/java/security/xss.yaml +++ b/rules/ruleset/java/security/xss.yaml @@ -2,7 +2,90 @@ rules: - id: xss-in-servlet-app severity: ERROR message: >- - Potential XSS: writing user input directly to a web page. + Cross-site scripting (XSS): untrusted input is written to a Servlet response with text/html + content type, so injected scripts will execute in the browser. + metadata: + cwe: CWE-79 + short-description: Cross-site scripting (XSS) in HTML response + full-description: |- + Cross-site scripting (XSS) occurs when untrusted input reaches a Servlet's response writer + without proper HTML encoding, allowing an attacker to inject client-side code (usually JavaScript) + into pages viewed by other users. Here the servlet explicitly sets the response content type + to `text/html`, so the browser will render the response as HTML and execute any injected scripts. + + Vulnerable example: + + ```java + import java.io.PrintWriter; + import javax.servlet.annotation.WebServlet; + import javax.servlet.http.HttpServlet; + import javax.servlet.http.HttpServletRequest; + import javax.servlet.http.HttpServletResponse; + + @WebServlet("/greet") + public class GreetingServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws java.io.IOException { + response.setContentType("text/html;charset=UTF-8"); + PrintWriter out = response.getWriter(); + + String name = request.getParameter("name"); // untrusted + + // VULNERABLE: tainted data reaches an HTML response unencoded + out.println("

Hello, " + name + "!

"); + } + } + ``` + + Safe example: + + ```java + import java.io.PrintWriter; + import javax.servlet.annotation.WebServlet; + import javax.servlet.http.HttpServlet; + import javax.servlet.http.HttpServletRequest; + import javax.servlet.http.HttpServletResponse; + import org.apache.commons.text.StringEscapeUtils; + + @WebServlet("/greet") + public class GreetingServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws java.io.IOException { + response.setContentType("text/html;charset=UTF-8"); + PrintWriter out = response.getWriter(); + + String name = request.getParameter("name"); + String safe = StringEscapeUtils.escapeHtml4(name == null ? "" : name); + + out.println("

Hello, " + safe + "!

"); + } + } + ``` + + Key vulnerable patterns covered by this rule include `PrintWriter.print*` / `write` and + `ServletOutputStream.print*` / `write` on responses whose content type is `text/html`, + plus `HttpServletResponse.sendError(code, message)` and `JspWriter.write*` calls with tainted data. + references: + - https://owasp.org/www-community/attacks/xss/ + provenance: + - https://github.com/semgrep/semgrep-rules/blob/develop/java/lang/security/audit/xss/no-direct-response-writer.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/servlet-xss-html-response-sinks.yaml#java-servlet-xss-html-response-sink + as: sink + on: + - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + + - id: response-injection-in-servlet-app + severity: NOTE + message: >- + Potential cross-site scripting (XSS): untrusted input is written to a Servlet response writer + without HTML encoding. metadata: cwe: CWE-79 short-description: Potential cross-site scripting (XSS) @@ -14,6 +97,12 @@ rules: Attackers can leverage this to steal session cookies, perform actions on behalf of victims, modify page content, or conduct phishing attacks within the trusted site context. + Make sure the response is actually rendered as HTML by the browser. This rule fires for any direct + write of untrusted input to a Servlet response writer; if the response is served with a non-HTML + content type (`application/json`, `text/plain`, an attachment, etc.) the impact is usually limited. + When the content type is statically confirmed to be `text/html`, the ERROR-tier sibling rule + `xss-in-servlet-app` reports the same flow at higher severity. + **Vulnerable code sample** ```java @@ -117,9 +206,9 @@ rules: - Avoid constructing HTML/JavaScript by string concatenation where possible; use safe templating or tag libraries that handle encoding (e.g., JSP ``). - Use security libraries and frameworks that provide standard encoding utilities. - references: - - https://owasp.org/www-community/attacks/xss/ - provenance: + references: + - https://owasp.org/www-community/attacks/xss/ + provenance: - https://github.com/semgrep/semgrep-rules/blob/develop/java/spring/security/injection/tainted-html-string.yaml - https://github.com/semgrep/semgrep-rules/blob/develop/java/lang/security/audit/xss/no-direct-response-writer.yaml languages: @@ -129,7 +218,7 @@ rules: refs: - rule: java/lib/generic/servlet-untrusted-data-source.yaml#java-servlet-untrusted-data-source as: untrusted-data - - rule: java/lib/generic/servlet-xss-sinks.yaml#java-servlet-xss-sink + - rule: java/lib/generic/servlet-response-injection-sinks.yaml#java-servlet-response-injection-sink as: sink on: - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' @@ -137,7 +226,102 @@ rules: - id: xss-in-spring-app severity: ERROR message: >- - Potential XSS: writing user input directly to a web page. + Cross-site scripting (XSS): untrusted input flows into a Spring response with text/html + content type, so injected scripts will execute in the browser. + metadata: + cwe: CWE-79 + short-description: Cross-site scripting (XSS) in HTML response + full-description: |- + Cross-site scripting (XSS) occurs when untrusted input is included in an HTML response without + proper encoding, allowing an attacker to inject client-side code (usually JavaScript) into pages + viewed by other users. In Spring applications, this happens when a handler writes untrusted input + directly to `HttpServletResponse` with an HTML content type, or returns untrusted input from a + handler annotated with `produces = "text/html"`. Here the response content type is statically + confirmed to be `text/html`, so the browser will render the response as HTML and execute any + injected scripts. + + Vulnerable example: + + ```java + import org.springframework.web.bind.annotation.GetMapping; + import org.springframework.web.bind.annotation.RequestParam; + import org.springframework.web.bind.annotation.RestController; + + @RestController + public class GreetingController { + @GetMapping(value = "/greet", produces = "text/html") + public String greet(@RequestParam String name) { + // VULNERABLE: tainted data returned from a text/html handler unencoded + return "

Hello, " + name + "!

"; + } + } + ``` + + Safe example — encode before returning HTML: + + ```java + import org.springframework.web.bind.annotation.GetMapping; + import org.springframework.web.bind.annotation.RequestParam; + import org.springframework.web.bind.annotation.RestController; + import org.springframework.web.util.HtmlUtils; + + @RestController + public class GreetingController { + @GetMapping(value = "/greet", produces = "text/html") + public String greet(@RequestParam String name) { + return "

Hello, " + HtmlUtils.htmlEscape(name) + "!

"; + } + } + ``` + + Safe example — return a typed DTO so Jackson serializes as JSON instead of HTML: + + ```java + import org.springframework.http.ResponseEntity; + import org.springframework.web.bind.annotation.GetMapping; + import org.springframework.web.bind.annotation.RequestParam; + import org.springframework.web.bind.annotation.RestController; + + @RestController + public class GreetingController { + public static class GreetingDto { + public String name; + public GreetingDto(String name) { this.name = name; } + } + + @GetMapping("/greet") + public ResponseEntity greet(@RequestParam(required = false, defaultValue = "") String name) { + // Jackson serializes the DTO as application/json — the browser will not render it as HTML + return ResponseEntity.ok(new GreetingDto(name)); + } + } + ``` + + Key vulnerable patterns covered by this rule include controller returns from handlers with + `produces = "text/html"`, direct writes to `HttpServletResponse.getWriter` / `getOutputStream` + from a handler that sets a `text/html` content type, and `ResponseEntity` bodies with an + HTML content type built from tainted strings. + references: + - https://owasp.org/www-community/attacks/xss/ + provenance: + - https://github.com/semgrep/semgrep-rules/blob/develop/java/spring/security/injection/tainted-html-string.yaml + languages: + - java + mode: join + join: + refs: + - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source + as: untrusted-data + - rule: java/lib/spring/spring-xss-html-response-sinks.yaml#spring-xss-html-response-sink + as: sink + on: + - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' + + - id: response-injection-in-spring-app + severity: NOTE + message: >- + Potential cross-site scripting (XSS): untrusted input is rendered in a Spring view or response + without HTML encoding. metadata: cwe: CWE-79 short-description: Potential cross-site scripting (XSS) @@ -149,6 +333,13 @@ rules: If the view engine renders user data as raw HTML, an attacker can inject scripts that execute in the victim's browser in the context of your application (stealing cookies, hijacking sessions, modifying content, etc.). + Make sure the response is actually rendered as HTML by the browser. This rule fires whenever + untrusted input flows into a Spring response or view; if the endpoint serves the response as + `application/json`, `text/plain`, or as an attachment, the impact is usually limited. When the + content type is statically confirmed to be `text/html` (for example via `produces = "text/html"` + or an explicit `setContentType`), the ERROR-tier sibling rule `xss-in-spring-app` reports the + same flow at higher severity. + **Vulnerable code sample** Controller: @@ -250,7 +441,7 @@ rules: refs: - rule: java/lib/spring/untrusted-data-source.yaml#spring-untrusted-data-source as: untrusted-data - - rule: java/lib/spring/spring-xss-sinks.yaml#spring-xss-sink + - rule: java/lib/spring/spring-response-injection-sinks.yaml#spring-response-injection-sink as: sink on: - 'untrusted-data.$UNTRUSTED -> sink.$UNTRUSTED' @@ -258,7 +449,6 @@ rules: - id: xssrequestwrapper-is-insecure severity: WARNING options: - # todo: clarify this pattern disabled: pattern with concrete class name message: >- It looks like you're using an implementation of XSSRequestWrapper from dzone. diff --git a/rules/test/src/main/java/security/xss/SafeDtoRestController.java b/rules/test/src/main/java/security/xss/SafeDtoRestController.java new file mode 100644 index 000000000..6e253e190 --- /dev/null +++ b/rules/test/src/main/java/security/xss/SafeDtoRestController.java @@ -0,0 +1,22 @@ +package security.xss; + +import org.opentaint.sast.test.util.NegativeRuleSample; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class SafeDtoRestController { + + public static class GreetingDto { + public String name; + public GreetingDto(String name) { this.name = name; } + } + + @GetMapping("/safe-dto") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity safeDto(@RequestParam(required = false, defaultValue = "") String name) { + return ResponseEntity.ok(new GreetingDto(name)); + } +} diff --git a/rules/test/src/main/java/security/xss/UnsafeBareBytesController.java b/rules/test/src/main/java/security/xss/UnsafeBareBytesController.java new file mode 100644 index 000000000..90e5b49c4 --- /dev/null +++ b/rules/test/src/main/java/security/xss/UnsafeBareBytesController.java @@ -0,0 +1,18 @@ +package security.xss; + +import java.nio.charset.StandardCharsets; + +import org.opentaint.sast.test.util.PositiveRuleSample; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class UnsafeBareBytesController { + + @GetMapping("/unsafe-bare-bytes") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public byte[] unsafeBareBytes(@RequestParam(required = false, defaultValue = "") String name) { + return ("

hi " + name + "

").getBytes(StandardCharsets.UTF_8); + } +} diff --git a/rules/test/src/main/java/security/xss/UnsafeBareResourceController.java b/rules/test/src/main/java/security/xss/UnsafeBareResourceController.java new file mode 100644 index 000000000..7f1bd5ed2 --- /dev/null +++ b/rules/test/src/main/java/security/xss/UnsafeBareResourceController.java @@ -0,0 +1,20 @@ +package security.xss; + +import java.nio.charset.StandardCharsets; + +import org.opentaint.sast.test.util.PositiveRuleSample; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class UnsafeBareResourceController { + + @GetMapping("/unsafe-bare-resource") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public Resource unsafeBareResource(@RequestParam(required = false, defaultValue = "") String name) { + return new ByteArrayResource(("

hi " + name + "

").getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/rules/test/src/main/java/security/xss/UnsafeHtmlRestController.java b/rules/test/src/main/java/security/xss/UnsafeHtmlRestController.java new file mode 100644 index 000000000..586fe85b7 --- /dev/null +++ b/rules/test/src/main/java/security/xss/UnsafeHtmlRestController.java @@ -0,0 +1,16 @@ +package security.xss; + +import org.opentaint.sast.test.util.PositiveRuleSample; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class UnsafeHtmlRestController { + + @GetMapping(value = "/unsafe-html", produces = "text/html") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String unsafeHtml(@RequestParam(required = false, defaultValue = "") String name) { + return "

Hello, " + name + "!

"; + } +} diff --git a/rules/test/src/main/java/security/xss/UnsafeResponseEntityIsrController.java b/rules/test/src/main/java/security/xss/UnsafeResponseEntityIsrController.java new file mode 100644 index 000000000..14f3a4e21 --- /dev/null +++ b/rules/test/src/main/java/security/xss/UnsafeResponseEntityIsrController.java @@ -0,0 +1,22 @@ +package security.xss; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import org.opentaint.sast.test.util.PositiveRuleSample; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class UnsafeResponseEntityIsrController { + + @GetMapping("/unsafe-responseentity-isr") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity unsafeResponseEntityIsr(@RequestParam(required = false, defaultValue = "") String name) { + byte[] bytes = ("

hi " + name + "

").getBytes(StandardCharsets.UTF_8); + return ResponseEntity.ok(new InputStreamResource(new ByteArrayInputStream(bytes))); + } +} diff --git a/rules/test/src/main/java/security/xss/UnsafeResponseEntityRawController.java b/rules/test/src/main/java/security/xss/UnsafeResponseEntityRawController.java new file mode 100644 index 000000000..dd87b997b --- /dev/null +++ b/rules/test/src/main/java/security/xss/UnsafeResponseEntityRawController.java @@ -0,0 +1,18 @@ +package security.xss; + +import org.opentaint.sast.test.util.PositiveRuleSample; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@SuppressWarnings({"rawtypes", "unchecked"}) +public class UnsafeResponseEntityRawController { + + @GetMapping("/unsafe-responseentity-raw") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity unsafeResponseEntityRaw(@RequestParam(required = false, defaultValue = "") String name) { + return ResponseEntity.ok("

hi " + name + "

"); + } +} diff --git a/rules/test/src/main/java/security/xss/UnsafeResponseEntityResourceController.java b/rules/test/src/main/java/security/xss/UnsafeResponseEntityResourceController.java new file mode 100644 index 000000000..a68e3c4b0 --- /dev/null +++ b/rules/test/src/main/java/security/xss/UnsafeResponseEntityResourceController.java @@ -0,0 +1,21 @@ +package security.xss; + +import java.nio.charset.StandardCharsets; + +import org.opentaint.sast.test.util.PositiveRuleSample; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class UnsafeResponseEntityResourceController { + + @GetMapping("/unsafe-responseentity-resource") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity unsafeResponseEntityResource(@RequestParam(required = false, defaultValue = "") String name) { + return ResponseEntity.ok(new ByteArrayResource(("

hi " + name + "

").getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/rules/test/src/main/java/security/xss/XssHtmlResponseServletSamples.java b/rules/test/src/main/java/security/xss/XssHtmlResponseServletSamples.java new file mode 100644 index 000000000..80616c50c --- /dev/null +++ b/rules/test/src/main/java/security/xss/XssHtmlResponseServletSamples.java @@ -0,0 +1,162 @@ +package security.xss; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.opentaint.sast.test.util.NegativeRuleSample; +import org.opentaint.sast.test.util.PositiveRuleSample; + +public class XssHtmlResponseServletSamples { + + @WebServlet("/xss-in-servlet-app/unsafe-html-explicit") + public static class UnsafeHtmlServlet extends HttpServlet { + + @Override + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-servlet-app") + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + response.setContentType("text/html;charset=UTF-8"); + PrintWriter out = response.getWriter(); + String name = request.getParameter("name"); + out.println("

Hello, " + name + "!

"); + } + } + + @WebServlet("/xss-in-servlet-app/unsafe-no-content-type") + public static class UnsafeNoContentTypeServlet extends HttpServlet { + + @Override + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-servlet-app") + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + PrintWriter out = response.getWriter(); + String name = request.getParameter("name"); + out.println("Hello, " + name + "!"); + } + } + + @WebServlet("/xss-in-servlet-app/unsafe-chained-writer") + public static class UnsafeChainedWriterServlet extends HttpServlet { + + @Override + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-servlet-app") + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String name = request.getParameter("name"); + response.getWriter().println("

Hello, " + name + "!

"); + } + } + + @WebServlet("/xss-in-servlet-app/unsafe-chained-output-stream") + public static class UnsafeChainedOutputStreamServlet extends HttpServlet { + + @Override + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-servlet-app") + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String name = request.getParameter("name"); + response.getOutputStream().write(("

Hello, " + name + "!

").getBytes()); + } + } + + @WebServlet("/xss-in-servlet-app/unsafe-output-stream-local") + public static class UnsafeOutputStreamLocalServlet extends HttpServlet { + + @Override + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-servlet-app") + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String name = request.getParameter("name"); + ServletOutputStream out = response.getOutputStream(); + out.write(("

Hello, " + name + "!

").getBytes()); + } + } + + @WebServlet("/xss-in-servlet-app/unsafe-send-error") + public static class UnsafeSendErrorServlet extends HttpServlet { + + @Override + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-servlet-app") + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String name = request.getParameter("name"); + response.sendError(400, "Bad input: " + name); + } + } + + @WebServlet("/xss-in-servlet-app/unsafe-typed-print-writer") + public static class UnsafeTypedPrintWriterServlet extends HttpServlet { + + @Override + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-servlet-app") + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String name = request.getParameter("name"); + PrintWriter out = obtainWriter(response); + out.println("

Hello, " + name + "!

"); + } + + private static PrintWriter obtainWriter(HttpServletResponse response) throws IOException { + return response.getWriter(); + } + } + + @WebServlet("/xss-in-servlet-app/safe-chained-writer-json") + public static class SafeChainedWriterJsonServlet extends HttpServlet { + + @Override + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-servlet-app") + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + response.setContentType("application/json;charset=UTF-8"); + String name = request.getParameter("name"); + response.getWriter().println("{\"greeting\": \"Hello, " + name + "\"}"); + } + } + + @WebServlet("/xss-in-servlet-app/safe-output-stream-octet") + public static class SafeOutputStreamOctetServlet extends HttpServlet { + + @Override + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-servlet-app") + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + response.setContentType("application/octet-stream"); + String name = request.getParameter("name"); + response.getOutputStream().write(name.getBytes()); + } + } + + @WebServlet("/xss-in-servlet-app/safe-html-explicit") + public static class SafeHtmlServlet extends HttpServlet { + + @Override + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-servlet-app") + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + response.setContentType("text/html;charset=UTF-8"); + PrintWriter out = response.getWriter(); + String name = request.getParameter("name"); + if (name == null) { name = ""; } + String safeName = org.apache.commons.text.StringEscapeUtils.escapeHtml4(name); + out.println("

Hello, " + safeName + "!

"); + } + } +} diff --git a/rules/test/src/main/java/security/xss/XssHtmlResponseSpringSamples.java b/rules/test/src/main/java/security/xss/XssHtmlResponseSpringSamples.java new file mode 100644 index 000000000..177afebf7 --- /dev/null +++ b/rules/test/src/main/java/security/xss/XssHtmlResponseSpringSamples.java @@ -0,0 +1,560 @@ +package security.xss; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; + +import javax.servlet.http.HttpServletResponse; + +import org.opentaint.sast.test.util.NegativeRuleSample; +import org.opentaint.sast.test.util.PositiveRuleSample; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.util.HtmlUtils; + +import java.nio.charset.Charset; + +public class XssHtmlResponseSpringSamples { + + @RestController + public static class UnsafeStringReturnController { + + @GetMapping("/xss-in-spring-app/unsafe-string-return") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String unsafeStringReturn(@RequestParam(required = false) String name) { + return "

Hello, " + name + "!

"; + } + } + + @Controller + public static class UnsafeResponseEntityStringController { + + @PostMapping("/xss-in-spring-app/unsafe-response-entity-string") + @ResponseBody + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity unsafeResponseEntityString(@RequestParam String filename) { + String errorMessage = "Conversion failed for " + filename; + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorMessage); + } + } + + @Controller + public static class UnsafeSetContentTypeController { + + @GetMapping("/xss-in-spring-app/unsafe-html") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void unsafeHtmlGreet(@RequestParam(required = false) String name, HttpServletResponse response) throws IOException { + response.setContentType("text/html;charset=UTF-8"); + PrintWriter out = response.getWriter(); + out.println("

Hello, " + name + "!

"); + } + } + + @Controller + public static class UnsafeSetHeaderController { + + @GetMapping("/xss-in-spring-app/unsafe-set-header") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void unsafeSetHeaderGreet(@RequestParam(required = false) String name, HttpServletResponse response) throws IOException { + response.setHeader("Content-Type", "text/html;charset=UTF-8"); + PrintWriter out = response.getWriter(); + out.println("

Hello, " + name + "!

"); + } + } + + @Controller + public static class SafeHtmlController { + + @GetMapping("/xss-in-spring-app/safe-html") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void safeHtmlGreet(@RequestParam(required = false, defaultValue = "") String name, HttpServletResponse response) throws IOException { + response.setContentType("text/html;charset=UTF-8"); + PrintWriter out = response.getWriter(); + String safeName = HtmlUtils.htmlEscape(name, "UTF-8"); + out.println("

Hello, " + safeName + "!

"); + } + } + + @Controller + public static class UnsafeResponseEntityBytesHtmlController { + + @GetMapping(value = "/xss-in-spring-app/unsafe-bytes-html", produces = "text/html") + @ResponseBody + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity unsafeBytesHtml(@RequestParam String name) { + byte[] body = ("

Hello, " + name + "!

").getBytes(Charset.defaultCharset()); + return ResponseEntity.status(HttpStatus.OK).body(body); + } + } + + @RestController + public static class SafeJsonStringReturnController { + + @GetMapping(value = "/xss-in-spring-app/safe-json-string", produces = "application/json") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String safeJsonStringReturn(@RequestParam(required = false, defaultValue = "") String name) { + return "{\"name\":\"" + name + "\"}"; + } + } + + @RestController + public static class SafeStringReturnController { + + @GetMapping("/xss-in-spring-app/safe-string-return") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String safeStringReturn(@RequestParam(required = false, defaultValue = "") String name) { + String safeName = HtmlUtils.htmlEscape(name, "UTF-8"); + return "

Hello, " + safeName + "!

"; + } + } + + @RestController + public static class Row02StringProducesHtmlController { + + @GetMapping(value = "/xss-in-spring-app/row-02", produces = "text/html") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String row02(@RequestParam(required = false, defaultValue = "") String name) { + return "

Hello, " + name + "!

"; + } + } + + @RestController + public static class Row04StringProducesTextPlainController { + + @GetMapping(value = "/xss-in-spring-app/row-04", produces = "text/plain") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String row04(@RequestParam(required = false, defaultValue = "") String name) { + return "Hello, " + name; + } + } + + @RestController + public static class Row05StringProducesPdfController { + + @GetMapping(value = "/xss-in-spring-app/row-05", produces = "application/pdf") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String row05(@RequestParam(required = false, defaultValue = "") String name) { + return "

Hello, " + name + "!

"; + } + } + + @RestController + public static class Row06StringProducesOctetStreamController { + + @GetMapping(value = "/xss-in-spring-app/row-06", produces = "application/octet-stream") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String row06(@RequestParam(required = false, defaultValue = "") String name) { + return "

Hello, " + name + "!

"; + } + } + + @RestController + public static class Row08ResponseEntityStringProducesJsonController { + + @GetMapping(value = "/xss-in-spring-app/row-08", produces = "application/json") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row08(@RequestParam(required = false, defaultValue = "") String name) { + return ResponseEntity.ok("{\"name\":\"" + name + "\"}"); + } + } + + @RestController + public static class Row09ResponseEntityStringContentTypeHtmlController { + + @GetMapping("/xss-in-spring-app/row-09") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row09(@RequestParam(required = false, defaultValue = "") String name) { + return ResponseEntity.ok() + .contentType(MediaType.TEXT_HTML) + .body("

Hello, " + name + "!

"); + } + } + + @RestController + public static class Row10ResponseEntityStringContentTypeJsonController { + + @GetMapping("/xss-in-spring-app/row-10") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row10(@RequestParam(required = false, defaultValue = "") String name) { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body("{\"name\":\"" + name + "\"}"); + } + } + + @RestController + public static class Row11ResponseEntityStringHeaderHtmlController { + + @GetMapping("/xss-in-spring-app/row-11") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row11(@RequestParam(required = false, defaultValue = "") String name) { + return ResponseEntity.ok() + .header("Content-Type", "text/html") + .body("

Hello, " + name + "!

"); + } + } + + @RestController + public static class Row12ResponseEntityStringHeaderJsonController { + + @GetMapping("/xss-in-spring-app/row-12") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row12(@RequestParam(required = false, defaultValue = "") String name) { + return ResponseEntity.ok() + .header("Content-Type", "application/json") + .body("{\"name\":\"" + name + "\"}"); + } + } + + @RestController + public static class Row13NewResponseEntityHeadersJsonController { + + @GetMapping("/xss-in-spring-app/row-13") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row13(@RequestParam(required = false, defaultValue = "") String name) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + return new ResponseEntity<>("{\"name\":\"" + name + "\"}", headers, HttpStatus.OK); + } + } + + @RestController + @SuppressWarnings({"rawtypes", "unchecked"}) + public static class Row14RawResponseEntityNoContentTypeController { + + @GetMapping("/xss-in-spring-app/row-14") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row14(@RequestParam(required = false, defaultValue = "") String name) { + return ResponseEntity.ok("

Hello, " + name + "!

"); + } + } + + @RestController + @SuppressWarnings({"rawtypes", "unchecked"}) + public static class Row15RawResponseEntityContentTypeJsonController { + + @GetMapping("/xss-in-spring-app/row-15") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row15(@RequestParam(required = false, defaultValue = "") String name) { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body("{\"name\":\"" + name + "\"}"); + } + } + + @RestController + public static class Row16StirlingPdfShapeController { + + @GetMapping("/xss-in-spring-app/row-16") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row16(@RequestParam(required = false, defaultValue = "") String filename) { + String err = "Conversion failed for " + filename; + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(err.getBytes(StandardCharsets.UTF_8)); + } + } + + @RestController + public static class Row18ResponseEntityBytesContentTypePdfController { + + @GetMapping("/xss-in-spring-app/row-18") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row18(@RequestParam(required = false, defaultValue = "") String name) { + byte[] body = ("PDF-1.4% fake for " + name).getBytes(StandardCharsets.UTF_8); + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_PDF) + .body(body); + } + } + + @RestController + public static class Row19ResponseEntityBytesContentTypeOctetStreamController { + + @GetMapping("/xss-in-spring-app/row-19") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row19(@RequestParam(required = false, defaultValue = "") String name) { + byte[] body = ("binary-for-" + name).getBytes(StandardCharsets.UTF_8); + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(body); + } + } + + @Controller + public static class Row20ServletSetContentTypeJsonController { + + @GetMapping("/xss-in-spring-app/row-20") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void row20(@RequestParam(required = false, defaultValue = "") String name, + HttpServletResponse response) throws IOException { + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + out.print("{\"name\":\"" + name + "\"}"); + } + } + + @Controller + public static class Row21ServletSetHeaderJsonController { + + @GetMapping("/xss-in-spring-app/row-21") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void row21(@RequestParam(required = false, defaultValue = "") String name, + HttpServletResponse response) throws IOException { + response.setHeader("Content-Type", "application/json"); + PrintWriter out = response.getWriter(); + out.print("{\"name\":\"" + name + "\"}"); + } + } + + @RestController + public static class Row22StringProducesMediaTypeJsonConstantController { + + @GetMapping(value = "/xss-in-spring-app/row-22", produces = MediaType.APPLICATION_JSON_VALUE) + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String row22(@RequestParam(required = false, defaultValue = "") String name) { + return "{\"payload\":\"" + name + "\"}"; + } + } + + @RestController + public static class Row23StringProducesMediaTypeTextHtmlConstantController { + + @GetMapping(value = "/xss-in-spring-app/row-23", produces = MediaType.TEXT_HTML_VALUE) + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String row23(@RequestParam(required = false, defaultValue = "") String name) { + return "

Hello, " + name + "!

"; + } + } + + @RestController + public static class Row24StringProducesApplicationXmlController { + + @GetMapping(value = "/xss-in-spring-app/row-24", produces = "application/xml") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String row24(@RequestParam(required = false, defaultValue = "") String name) { + return "" + name + ""; + } + } + + @RestController + public static class Row25StringProducesSvgController { + + @GetMapping(value = "/xss-in-spring-app/row-25", produces = "image/svg+xml") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String row25(@RequestParam(required = false, defaultValue = "") String name) { + return "" + + "" + name + "" + + ""; + } + } + + @RestController + public static class Row27DeferredResultStringController { + + @GetMapping("/xss-in-spring-app/row-27") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public DeferredResult row27(@RequestParam(required = false, defaultValue = "") String name) { + DeferredResult result = new DeferredResult<>(); + result.setResult("

Hello, " + name + "!

"); + return result; + } + } + + @RestController + public static class Row28CompletableFutureStringController { + + @GetMapping("/xss-in-spring-app/row-28") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public CompletableFuture row28(@RequestParam(required = false, defaultValue = "") String name) { + return CompletableFuture.completedFuture("

Hello, " + name + "!

"); + } + } + + @Controller + public static class Row30ServletSetContentTypeHtmlUtf16Controller { + + @GetMapping("/xss-in-spring-app/row-30") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void row30(@RequestParam(required = false, defaultValue = "") String name, + HttpServletResponse response) throws IOException { + response.setContentType("text/html;charset=utf-16"); + PrintWriter out = response.getWriter(); + out.println("

Hello, " + name + "!

"); + } + } + + @RestController + @org.springframework.web.bind.annotation.RequestMapping(value = "/xss-in-spring-app/row-31", produces = "application/json") + public static class Row31RestControllerClassLevelJsonController { + + @GetMapping + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String row31(@RequestParam(required = false, defaultValue = "") String name) { + return "{\"name\":\"" + name + "\"}"; + } + } + + @RestController + @org.springframework.web.bind.annotation.RequestMapping(value = "/xss-in-spring-app/row-32", produces = "text/html") + public static class Row32RestControllerClassLevelHtmlController { + + @GetMapping + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String row32(@RequestParam(required = false, defaultValue = "") String name) { + return "

Hello, " + name + "!

"; + } + } + + @org.springframework.stereotype.Controller + public static class Row34ServletSetHeaderTextHtmlController { + + @GetMapping("/xss-in-spring-app/row-34") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void row34(@RequestParam(required = false, defaultValue = "") String name, + HttpServletResponse response) throws IOException { + response.setHeader("Content-Type", "text/html"); + PrintWriter out = response.getWriter(); + out.println("

Hello, " + name + "!

"); + } + } + + @org.springframework.stereotype.Controller + public static class Row35ServletAddHeaderJsonController { + + @GetMapping("/xss-in-spring-app/row-35") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void row35(@RequestParam(required = false, defaultValue = "") String name, + HttpServletResponse response) throws IOException { + response.addHeader("Content-Type", "application/json"); + PrintWriter out = response.getWriter(); + out.print("{\"name\":\"" + name + "\"}"); + } + } + + @RestController + public static class Row36ResponseEntityAssignmentJsonController { + + @GetMapping("/xss-in-spring-app/row-36") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public ResponseEntity row36(@RequestParam(required = false, defaultValue = "") String name) { + ResponseEntity result = ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body("{\"name\":\"" + name + "\"}"); + return result; + } + } + + @org.springframework.stereotype.Controller + public static class Row37ServletSetContentTypeJsonAssignmentController { + + @GetMapping("/xss-in-spring-app/row-37") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void row37(@RequestParam(required = false, defaultValue = "") String name, + HttpServletResponse response) throws IOException { + response.setContentType("application/json"); + String body = "{\"name\":\"" + name + "\"}"; + PrintWriter out = response.getWriter(); + out.print(body); + } + } + + @org.springframework.stereotype.Controller + public static class Row50ServletSetContentTypeHtmlConstantController { + + @GetMapping("/xss-in-spring-app/row-50") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void row50(@RequestParam(required = false, defaultValue = "") String name, + HttpServletResponse response) throws IOException { + response.setContentType(MediaType.TEXT_HTML_VALUE); + PrintWriter out = response.getWriter(); + out.println("

Hello, " + name + "!

"); + } + } + + @org.springframework.stereotype.Controller + public static class Row51ServletSetHeaderHtmlConstantController { + + @GetMapping("/xss-in-spring-app/row-51") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void row51(@RequestParam(required = false, defaultValue = "") String name, + HttpServletResponse response) throws IOException { + response.setHeader("Content-Type", MediaType.TEXT_HTML_VALUE); + PrintWriter out = response.getWriter(); + out.println("

Hello, " + name + "!

"); + } + } + + @org.springframework.stereotype.Controller + public static class Row52ServletAddHeaderHtmlConstantController { + + @GetMapping("/xss-in-spring-app/row-52") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public void row52(@RequestParam(required = false, defaultValue = "") String name, + HttpServletResponse response) throws IOException { + response.addHeader("Content-Type", MediaType.TEXT_HTML_VALUE); + PrintWriter out = response.getWriter(); + out.println("

Hello, " + name + "!

"); + } + } + + @RestController + public static class Row53BuilderChainHtmlObjectReturnController { + + @GetMapping("/xss-in-spring-app/row-53") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public Object row53(@RequestParam(required = false, defaultValue = "") String name) { + return ResponseEntity.ok() + .contentType(MediaType.TEXT_HTML) + .body("

Hello, " + name + "!

"); + } + } + + @RestController + public static class Row54BuilderChainHtmlMultiStatementController { + + @GetMapping("/xss-in-spring-app/row-54") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public Object row54(@RequestParam(required = false, defaultValue = "") String name) { + ResponseEntity entity = ResponseEntity.ok() + .contentType(MediaType.TEXT_HTML) + .body("

Hello, " + name + "!

"); + return entity; + } + } + + @RestController + public static class Row55BuilderChainHtmlEntityDiscardedController { + + @GetMapping("/xss-in-spring-app/row-55") + @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + @SuppressWarnings("unused") + public void row55(@RequestParam(required = false, defaultValue = "") String name) { + ResponseEntity entity = ResponseEntity.ok() + .contentType(MediaType.TEXT_HTML) + .body("

Hello, " + name + "!

"); + + } + } + + @Controller + public static class Row56ControllerResponseBodyStringController { + + @GetMapping("/xss-in-spring-app/row-56") + @ResponseBody + @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") + public String row56(@RequestParam(required = false, defaultValue = "") String name) { + return "

Hello, " + name + "!

"; + } + } +} diff --git a/rules/test/src/main/java/security/xss/XssServletSamples.java b/rules/test/src/main/java/security/xss/XssServletSamples.java index 61a11ea7f..3be72e68f 100644 --- a/rules/test/src/main/java/security/xss/XssServletSamples.java +++ b/rules/test/src/main/java/security/xss/XssServletSamples.java @@ -12,14 +12,8 @@ import org.opentaint.sast.test.util.NegativeRuleSample; import org.opentaint.sast.test.util.PositiveRuleSample; -/** - * Servlet-based samples for xss-in-servlet-app. - */ public class XssServletSamples { - /** - * Unsafe servlet that writes untrusted input directly into the HTML response without encoding. - */ @WebServlet("/xss-in-servlet-app/unsafe") public static class UnsafeGreetingServlet extends HttpServlet { @@ -31,22 +25,17 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); - // Untrusted input taken directly from the request String name = request.getParameter("name"); - // VULNERABLE: Unencoded user input is directly embedded into HTML out.println(""); out.println("Greeting"); out.println(""); - out.println("

Hello, " + name + "!

"); // XSS if 'name' contains HTML/JS + out.println("

Hello, " + name + "!

"); out.println(""); out.println(""); } } - /** - * Safe servlet that encodes untrusted input before including it in the HTML response. - */ @WebServlet("/xss-in-servlet-app/safe") public static class SafeGreetingServlet extends HttpServlet { @@ -63,7 +52,6 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) name = ""; } - // Encode untrusted input for HTML context String safeName = org.apache.commons.text.StringEscapeUtils.escapeHtml4(name); out.println(""); @@ -74,4 +62,19 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) out.println(""); } } + + @WebServlet("/response-injection-in-servlet-app/unsafe-json") + public static class UnsafeJsonInfoServlet extends HttpServlet { + + @Override + @PositiveRuleSample(value = "java/security/xss.yaml", id = "response-injection-in-servlet-app") + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + response.setContentType("application/json;charset=UTF-8"); + PrintWriter out = response.getWriter(); + String name = request.getParameter("name"); + out.println("{\"greeting\": \"Hello, " + name + "\"}"); + } + } } diff --git a/rules/test/src/main/java/security/xss/XssSpringSamples.java b/rules/test/src/main/java/security/xss/XssSpringSamples.java index b26eafc6c..9a4af1c76 100644 --- a/rules/test/src/main/java/security/xss/XssSpringSamples.java +++ b/rules/test/src/main/java/security/xss/XssSpringSamples.java @@ -2,37 +2,31 @@ import java.io.IOException; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import javax.servlet.http.HttpServletResponse; import org.opentaint.sast.test.util.NegativeRuleSample; import org.opentaint.sast.test.util.PositiveRuleSample; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.util.HtmlUtils; - - -/** - * Spring MVC samples for xss-in-spring-app. - */ public class XssSpringSamples { @Controller public static class UnsafeXssSpringController { - /** - * Unsafe endpoint that writes untrusted data directly into the HTTP response without encoding. - * This models a direct (reflected) XSS, which the rule is meant to detect. - */ @GetMapping("/xss-in-spring-app/unsafe") @PositiveRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") public void unsafeGreet(@RequestParam(required = false) String name, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); - // VULNERABLE: untrusted input is written directly to the page out.println(""); out.println(""); out.println("

Hello, " + name + "!

"); @@ -41,14 +35,9 @@ public void unsafeGreet(@RequestParam(required = false) String name, HttpServlet } } - @Controller public static class SafeXssSpringController { - /** - * Safe endpoint that encodes user input before writing it to the HTTP response, - * so no direct untrusted data flow reaches the page. - */ @GetMapping("/xss-in-spring-app/safe") @NegativeRuleSample(value = "java/security/xss.yaml", id = "xss-in-spring-app") public void safeGreet(@RequestParam(required = false, defaultValue = "") String name, HttpServletResponse response) throws IOException { @@ -56,7 +45,6 @@ public void safeGreet(@RequestParam(required = false, defaultValue = "") String name = ""; } - // Use Spring's standard HTML escaper for safe output encoding String safeName = HtmlUtils.htmlEscape(name, "UTF-8"); response.setContentType("text/html;charset=UTF-8"); @@ -71,4 +59,28 @@ public void safeGreet(@RequestParam(required = false, defaultValue = "") String } } + @Controller + public static class UnsafeNoContentTypeSpringController { + + @GetMapping("/xss-in-spring-app/unsafe-no-content-type") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "response-injection-in-spring-app") + public void unsafeNoContentType(@RequestParam(required = false) String name, HttpServletResponse response) throws IOException { + PrintWriter out = response.getWriter(); + + out.println("Hello, " + name + "!"); + } + } + + @Controller + public static class UnsafeResponseEntityController { + + @PostMapping("/xss-in-spring-app/unsafe-response-entity") + @PositiveRuleSample(value = "java/security/xss.yaml", id = "response-injection-in-spring-app") + public ResponseEntity unsafeResponseEntity(@RequestParam String filename) { + String errorMessage = "Conversion failed for " + filename; + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorMessage.getBytes(StandardCharsets.UTF_8)); + } + } + }