diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md
index 5763d34..2745f6a 100644
--- a/docs/VULNERABILITY_CATALOG.md
+++ b/docs/VULNERABILITY_CATALOG.md
@@ -7,12 +7,12 @@ from each file's header comment, so this page cannot drift from the source.
## Totals
-- **Test cases:** 62
-- **Expected detections:** 62
-- **`VULNERABLE:` markers:** 123 (individual lines a scanner should flag)
-- **`SAFE:` markers:** 73 (lines a scanner must not flag — the false-positive control group)
+- **Test cases:** 63
+- **Expected detections:** 63
+- **`VULNERABLE:` markers:** 124 (individual lines a scanner should flag)
+- **`SAFE:` markers:** 74 (lines a scanner must not flag — the false-positive control group)
- **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text
-- **CWE categories:** 46 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-95, CWE-113, CWE-117, CWE-190, CWE-201, CWE-209, CWE-256, CWE-295, CWE-321, CWE-327, CWE-330, CWE-338, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-798, CWE-862, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357
+- **CWE categories:** 47 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-91, CWE-95, CWE-113, CWE-117, CWE-190, CWE-201, CWE-209, CWE-256, CWE-295, CWE-321, CWE-327, CWE-330, CWE-338, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-798, CWE-862, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357
## How coverage is scored
@@ -37,6 +37,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`.
|---|---|---|---|---|---|
| Insecure deserialisation via ObjectInputStream | [`deserialization-object-input-stream.java`](../vulns/java/deserialization-object-input-stream.java) | CWE-502 | critical | yes | 2 vuln / 2 safe |
| SQL injection via Statement string concatenation | [`sqli-statement-concat.java`](../vulns/java/sqli-statement-concat.java) | CWE-89 | critical | yes | 2 vuln / 1 safe |
+| XPath injection via unvalidated user input in XPath expression | [`cwe-91-java.java`](../vulns/java/cwe-91-java.java) | CWE-91 | high | yes | 1 vuln / 1 safe |
| Reflected XSS via HttpServletResponse writer | [`xss-response-writer.java`](../vulns/java/xss-response-writer.java) | CWE-79 | high | yes | 2 vuln / 2 safe |
| XXE via unconfigured DocumentBuilderFactory | [`xxe-document-builder.java`](../vulns/java/xxe-document-builder.java) | CWE-611 | high | yes | 2 vuln / 1 safe |
diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json
index 153fdaf..8e37be2 100644
--- a/vulns/VULNERABILITY_CATALOG.json
+++ b/vulns/VULNERABILITY_CATALOG.json
@@ -2,10 +2,10 @@
"schema": "threatcrush-testbed-catalog/1",
"note": "Generated by scripts/generate-catalog.py \u2014 do not edit by hand.",
"totals": {
- "test_cases": 62,
- "expected_detections": 62,
- "vulnerable_markers": 123,
- "safe_markers": 73,
+ "test_cases": 63,
+ "expected_detections": 63,
+ "vulnerable_markers": 124,
+ "safe_markers": 74,
"languages": [
"dotenv",
"go",
@@ -32,6 +32,7 @@
"CWE-79",
"CWE-89",
"CWE-90",
+ "CWE-91",
"CWE-95",
"CWE-113",
"CWE-117",
@@ -175,6 +176,29 @@
56
]
},
+ {
+ "id": "java-xpath-injection-document-builder",
+ "file": "vulns/java/cwe-91-java.java",
+ "title": "XPath injection via unvalidated user input in XPath expression",
+ "category": "java",
+ "language": "java",
+ "cwe": "CWE-91",
+ "cwes": [
+ "CWE-91"
+ ],
+ "severity": "high",
+ "expected_detection": true,
+ "description": "User-controlled input from an HTTP request parameter is",
+ "detection_target": "XPathExpression.evaluate reached from untrusted input",
+ "safe_guard": "Every payload is behind the always-false NEVER_RUN constant and",
+ "attribution": "line",
+ "vulnerable_lines": [
+ 51
+ ],
+ "safe_lines": [
+ 76
+ ]
+ },
{
"id": "java-deserialization-object-input-stream",
"file": "vulns/java/deserialization-object-input-stream.java",
diff --git a/vulns/java/cwe-91-java.java b/vulns/java/cwe-91-java.java
new file mode 100644
index 0000000..43186d6
--- /dev/null
+++ b/vulns/java/cwe-91-java.java
@@ -0,0 +1,88 @@
+/**
+ * @id java-xpath-injection-document-builder
+ * @test-case XPath injection via unvalidated user input in XPath expression
+ * @cwe CWE-91
+ * @severity high
+ * @language java
+ * @expected-detection true
+ * @description User-controlled input from an HTTP request parameter is
+ * concatenated directly into an XPath query string used to
+ * evaluate an XML document. An attacker can inject XPath
+ * expressions to bypass authentication or extract sensitive
+ * data from the XML document, leading to information disclosure
+ * or authorization bypass.
+ * @safe-guard Every payload is behind the always-false NEVER_RUN constant and
+ * the repository has no Java build file, so nothing is compiled.
+ * No malicious XPath payload is included in this file.
+ * @detection-target XPathExpression.evaluate reached from untrusted input
+ * without parameterized XPath queries or input validation.
+ *
+ * NEVER RUN IN PRODUCTION — intentional test case for scanner validation.
+ */
+package vulns.java;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathFactory;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import java.io.StringReader;
+import javax.servlet.http.HttpServletRequest;
+
+public class XPathInjectionDocumentBuilder {
+
+ private static final boolean NEVER_RUN = false;
+
+ public String evaluateUserQueryVulnerable(HttpServletRequest req) throws Exception {
+ if (NEVER_RUN) {
+ String userInput = req.getParameter("username"); // SOURCE
+ String xml = "adminadmin"
+ + "guestguest";
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document doc = builder.parse(new InputSource(new StringReader(xml)));
+
+ XPathFactory xPathFactory = XPathFactory.newInstance();
+ XPath xpath = xPathFactory.newXPath();
+ // VULNERABLE: CWE-91 - user input concatenated into XPath expression
+ String expression = "//user[username='" + userInput + "']/role/text()";
+ XPathExpression expr = xpath.compile(expression);
+ return (String) expr.evaluate(doc, XPathConstants.STRING);
+ }
+ return null;
+ }
+
+ /**
+ * Safe counterpart — the scanner should NOT flag this.
+ *
+ * @expected-detection false
+ */
+ public String evaluateUserQuerySafe(HttpServletRequest req) throws Exception {
+ if (NEVER_RUN) {
+ String userInput = req.getParameter("username");
+ String xml = "adminadmin"
+ + "guestguest";
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document doc = builder.parse(new InputSource(new StringReader(xml)));
+
+ XPathFactory xPathFactory = XPathFactory.newInstance();
+ XPath xpath = xPathFactory.newXPath();
+ // SAFE: parameterized XPath query with variable binding
+ XPathExpression expr = xpath.compile("//user[username=$input]/role/text()");
+ expr.setXPathVariableResolver(variable -> {
+ if ("input".equals(variable.getLocalName())) {
+ return userInput;
+ }
+ return null;
+ });
+ return (String) expr.evaluate(doc, XPathConstants.STRING);
+ }
+ return null;
+ }
+}
\ No newline at end of file