{"repro_id":"REPRO-2026-00280","version":6,"title":"Apache Tomcat examples app XSS in numguess.jsp","repro_type":"security","status":"published","severity":"low","description":"CVE-2026-50229 is a reflected/stored XSS issue in the bundled examples web application shipped with Apache Tomcat. The vulnerable JSP webapps/examples/jsp/num/numguess.jsp used wildcard bean property binding (<jsp:setProperty ... property=\"*\"/>) against a session-scoped bean, and the page later rendered the bean's hint value into HTML without escaping. Apache fixed the issue by restricting property binding to the intended guess parameter. Affected versions include 7.0.0 through 7.0.109, 8.5.0 through 8.5.100, 9.0.0.M1 through 9.0.118, 10.1.0-M1 through 10.1.55, and 11.0.0-M1 through 11.0.22. Fixed in 11.0.23, 10.1.56, and 9.0.119; 8.5 and 7.0 are end-of-life.","root_cause":"# Root Cause Analysis: CVE-2026-50229\n\n## Summary\n\nCVE-2026-50229 is a reflected/stored cross-site scripting (XSS) vulnerability in the bundled `examples` web application of Apache Tomcat. The vulnerable JSP page `webapps/examples/jsp/num/numguess.jsp` uses wildcard bean property binding (`<jsp:setProperty name=\"numguess\" property=\"*\"/>`) against a session-scoped `NumberGuessBean`. Because the `NumberGuessBean` exposes a `hint` property, an attacker can supply a `hint` request parameter that overwrites the bean's intended hint value. The page later renders the bean's `hint` value directly into the HTML response using `<%= numguess.getHint() %>`, which does not escape the content. This allows an attacker to inject arbitrary JavaScript/HTML into the response.\n\n## Impact\n\n- **Product / Component:** Apache Tomcat, bundled `examples` web application (`webapps/examples/jsp/num/numguess.jsp`)\n- **Affected versions:** 7.0.0 through 7.0.109, 8.5.0 through 8.5.100, 9.0.0.M1 through 9.0.118, 10.1.0-M1 through 10.1.55, and 11.0.0-M1 through 11.0.22\n- **Fixed versions:** 11.0.23, 10.1.56, 9.0.119 (8.5 and 7.0 are end-of-life)\n- **Risk level / consequences:** Medium. An attacker who can trick a user into visiting a crafted URL can execute JavaScript in the user's browser session in the context of the Tomcat examples application. Because the bean is session-scoped, the payload is also persisted until the session is reset, giving a stored-like behavior.\n\n## Impact Parity\n\n- **Disclosed / claimed maximum impact:** XSS in the bundled Tomcat examples web application via attacker-controlled request parameters.\n- **Reproduced impact from this run:** A live HTTP request to the `numguess.jsp` endpoint on Tomcat 10.1.55 rendered the unescaped `<script>alert(1)</script>` payload in the HTML response. The same request against Tomcat 10.1.56 did not render the payload because the `hint` parameter is no longer bound into the session bean.\n- **Parity:** `full` — the reproduced behavior matches the disclosed XSS impact.\n\n## Root Cause\n\nThe root cause is the combination of two design choices in `numguess.jsp`:\n\n1. **Wildcard bean property binding:**\n   ```jsp\n   <jsp:useBean id=\"numguess\" class=\"num.NumberGuessBean\" scope=\"session\"/>\n   <jsp:setProperty name=\"numguess\" property=\"*\"/>\n   ```\n   `property=\"*\"` binds every request parameter to a bean property of the same name. This exposes properties other than the intended `guess` parameter (e.g., `hint`, `answer`, `success`, `numGuesses`).\n\n2. **Unescaped output:**\n   ```jsp\n   Good guess, but nope. Try <b><%= numguess.getHint() %></b>.\n   ```\n   The `hint` value is emitted directly into the HTML without escaping. When an attacker sets `hint` to HTML/JavaScript, the browser parses and executes it.\n\nApache fixed this by restricting the property binding to the intended `guess` parameter:\n\n```diff\n-<jsp:setProperty name=\"numguess\" property=\"*\"/>\n+<jsp:setProperty name=\"numguess\" property=\"guess\"/>\n```\n\nFix commit: `0d5bdd5b0dd964e9f73e530b7d753462b9bfd1d0` (\"Minor optimisation. Only need to set 1 property so don't use wild card.\")\n\n## Reproduction Steps\n\n1. Run `bundle/repro/reproduction_steps.sh`.\n2. The script installs Java if needed, downloads and extracts Apache Tomcat 10.1.55 (vulnerable) and 10.1.56 (fixed), then configures and starts each instance in turn on port `18080`.\n3. For each version, the script performs two requests in the same HTTP session:\n   - First request: `GET /examples/jsp/num/numguess.jsp?guess=abc` — establishes a session and advances the game state so the hint branch is rendered.\n   - Second request: `GET /examples/jsp/num/numguess.jsp?hint=%3Cscript%3Ealert(1)%3C%2Fscript%3E` — attempts to bind the attacker-controlled `hint` parameter into the session bean.\n4. The script inspects the second response for the literal string `<script>alert(1)</script>`.\n\nExpected evidence:\n- **Vulnerable (10.1.55):** the second response contains `Good guess, but nope. Try <b><script>alert(1)</script></b>.`\n- **Fixed (10.1.56):** the second response contains the default hint (e.g., `a number next time`) and no attacker-controlled markup.\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — full script execution log.\n- `bundle/repro/vulnerable_resp2.html` — HTTP response from the vulnerable Tomcat 10.1.55 showing the unescaped payload.\n- `bundle/repro/fixed_resp2.html` — HTTP response from the fixed Tomcat 10.1.56 showing the payload is absent.\n- `bundle/repro/runtime_manifest.json` — runtime evidence manifest.\n\nKey excerpt from `vulnerable_resp2.html`:\n```html\nGood guess, but nope.  Try <b><script>alert(1)</script></b>.\n```\n\nKey excerpt from `fixed_resp2.html`:\n```html\nGood guess, but nope.  Try <b>a number next time</b>.\n```\n\nEnvironment:\n- OpenJDK 25.0.3 (installed via `default-jdk` package)\n- Apache Tomcat 10.1.55 and 10.1.56 binary distributions from `https://archive.apache.org/dist/tomcat/`\n\n## Recommendations / Next Steps\n\n- **Upgrade** to a fixed Tomcat version (10.1.56+, 9.0.119+, or 11.0.23+).\n- **Remove or disable** the `examples` web application in production environments; it is intended for demonstration only.\n- **Avoid wildcard bean property binding** (`property=\"*\"`) when the request can contain untrusted parameters; explicitly whitelist the parameters the application intends to bind.\n- **Escape output** when rendering dynamic content, or use a framework that escapes expression-language output by default.\n- Add regression tests that verify untrusted request parameters are not bound into session beans and are not reflected unescaped in responses.\n\n## Additional Notes\n\n- The reproduction script is idempotent: it reuses cached Tomcat archives, reconfigures ports deterministically, and cleanly stops each instance before starting the next one.\n- The script was run twice consecutively and produced the same confirmed result both times.\n- The examples application is typically not deployed in production, which limits real-world exploitability, but the vulnerability is real in the shipped product.\n","cve_id":"CVE-2026-50229","cwe_id":"CWE-80","source_url":"https://tomcat.apache.org/security-11.html","package":{"name":"apache/tomcat","ecosystem":"github","affected_versions":"Apache Tomcat 11.0.0-M1 through 11.0.22; 10.1.0-M1 through 10.1.55; 9.0.0.M1 through 9.0.118; 8.5.0 through 8.5.100; 7.0.0 through 7.0.109"},"reproduced_at":"2026-07-09T19:34:04.388333+00:00","duration_secs":966.0,"tool_calls":146,"handoffs":2,"total_cost_usd":1.4185653499999995,"agent_costs":{"hypothesis_generator":0.0179453,"judge":0.185823,"repro":0.20307679,"support":0.10793507,"vuln_variant":0.90378519},"cost_breakdown":{"hypothesis_generator":{"accounts/fireworks/models/kimi-k2p7-code":0.0179453},"judge":{"gpt-5.5":0.185823},"repro":{"accounts/fireworks/models/kimi-k2p7-code":0.20307679},"support":{"accounts/fireworks/models/kimi-k2p7-code":0.10793507},"vuln_variant":{"accounts/fireworks/models/kimi-k2p7-code":0.90378519}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-09T19:34:32.628389+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":6053,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":6053,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":8725,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":8326,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":11661,"category":"other"},{"path":"bundle/artifact_promotion_report.json","filename":"artifact_promotion_report.json","size":11679,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":892,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1195,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":718,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":576,"category":"other"},{"path":"bundle/logs/vulnerable_startup.log","filename":"vulnerable_startup.log","size":48,"category":"log"},{"path":"bundle/logs/vulnerable_curl.log","filename":"vulnerable_curl.log","size":0,"category":"log"},{"path":"bundle/repro/vulnerable_resp2.html","filename":"vulnerable_resp2.html","size":384,"category":"other"},{"path":"bundle/repro/fixed_resp2.html","filename":"fixed_resp2.html","size":377,"category":"other"},{"path":"bundle/logs/vuln_variant.log","filename":"vuln_variant.log","size":8577,"category":"log"},{"path":"bundle/vuln_variant/vulnerable_numguess_resp2.html","filename":"vulnerable_numguess_resp2.html","size":384,"category":"other"},{"path":"bundle/vuln_variant/fixed_numguess_resp2.html","filename":"fixed_numguess_resp2.html","size":377,"category":"other"},{"path":"bundle/vuln_variant/fixed_colors_resp1.html","filename":"fixed_colors_resp1.html","size":383,"category":"other"},{"path":"bundle/vuln_variant/fixed_carts_resp1.html","filename":"fixed_carts_resp1.html","size":795,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":5688,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":3424,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":1132,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1259,"category":"other"},{"path":"bundle/logs/variant_vulnerable_numguess_startup.log","filename":"variant_vulnerable_numguess_startup.log","size":48,"category":"log"},{"path":"bundle/logs/variant_vulnerable_numguess_curl.log","filename":"variant_vulnerable_numguess_curl.log","size":0,"category":"log"},{"path":"bundle/logs/variant_fixed_numguess_startup.log","filename":"variant_fixed_numguess_startup.log","size":48,"category":"log"},{"path":"bundle/logs/variant_fixed_numguess_curl.log","filename":"variant_fixed_numguess_curl.log","size":0,"category":"log"},{"path":"bundle/logs/variant_fixed_colors_startup.log","filename":"variant_fixed_colors_startup.log","size":48,"category":"log"},{"path":"bundle/logs/variant_fixed_colors_curl.log","filename":"variant_fixed_colors_curl.log","size":0,"category":"log"},{"path":"bundle/logs/variant_fixed_carts_startup.log","filename":"variant_fixed_carts_startup.log","size":48,"category":"log"},{"path":"bundle/logs/variant_fixed_carts_curl.log","filename":"variant_fixed_carts_curl.log","size":0,"category":"log"}]}