CVE-2026-50229: Verified Repro With Script Download
CVE-2026-50229: Apache Tomcat examples app XSS in numguess.jsp
CVE-2026-50229 is verified against apache/tomcat · 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. Vulnerability class: XSS. This medium reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00280.
What Is CVE-2026-50229?
CVE-2026-50229 is a low-severity reflected/stored cross-site scripting (XSS) vulnerability (CWE-80) in the bundled examples web application shipped with Apache Tomcat. Pruva reproduced it (reproduction REPRO-2026-00280).
CVE-2026-50229 Severity & CVSS Score
CVE-2026-50229 is rated medium severity, with a CVSS base score of 6.1 out of 10.
Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.
Affected apache/tomcat Versions
apache/tomcat · github 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 are affected.
How to Reproduce CVE-2026-50229
pruva-verify REPRO-2026-00280 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00280/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-50229
- reached the target end-to-end
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
HTTP GET parameter hint containing <script>alert(1)</script>
- /examples/jsp/num/numguess.jsp in the bundled Tomcat examples web application
reproduction_steps.sh No distinct bypass or alternate XSS trigger found for CVE-2026-50229. The fix in numguess.jsp correctly restricts bean binding to the guess parameter. Two other example JSPs (colrs.jsp, carts.jsp) still use wildcard binding but were confirmed not to render unescaped attacker-controlled markup on the fixed release.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-50229
CVE-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.
- Product / Component: Apache Tomcat, bundled
examplesweb application (webapps/examples/jsp/num/numguess.jsp) - 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
- Fixed versions: 11.0.23, 10.1.56, 9.0.119 (8.5 and 7.0 are end-of-life)
- 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.
Impact Parity
- Disclosed / claimed maximum impact: XSS in the bundled Tomcat examples web application via attacker-controlled request parameters.
- Reproduced impact from this run: A live HTTP request to the
numguess.jspendpoint 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 thehintparameter is no longer bound into the session bean. - Parity:
full— the reproduced behavior matches the disclosed XSS impact.
Root Cause
The root cause is the combination of two design choices in numguess.jsp:
Wildcard bean property binding:
<jsp:useBean id="numguess" class="num.NumberGuessBean" scope="session"/> <jsp:setProperty name="numguess" property="*"/>property="*"binds every request parameter to a bean property of the same name. This exposes properties other than the intendedguessparameter (e.g.,hint,answer,success,numGuesses).Unescaped output:
Good guess, but nope. Try <b><%= numguess.getHint() %></b>.The
hintvalue is emitted directly into the HTML without escaping. When an attacker setshintto HTML/JavaScript, the browser parses and executes it.
Apache fixed this by restricting the property binding to the intended guess parameter:
-<jsp:setProperty name="numguess" property="*"/>
+<jsp:setProperty name="numguess" property="guess"/>
Fix commit: 0d5bdd5b0dd964e9f73e530b7d753462b9bfd1d0 ("Minor optimisation. Only need to set 1 property so don't use wild card.")
Reproduction Steps
- Run
bundle/repro/reproduction_steps.sh. - 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. - For each version, the script performs two requests in the same HTTP session:
- First request:
GET /examples/jsp/num/numguess.jsp?guess=abc— establishes a session and advances the game state so the hint branch is rendered. - Second request:
GET /examples/jsp/num/numguess.jsp?hint=%3Cscript%3Ealert(1)%3C%2Fscript%3E— attempts to bind the attacker-controlledhintparameter into the session bean.
- First request:
- The script inspects the second response for the literal string
<script>alert(1)</script>.
Expected evidence:
- Vulnerable (10.1.55): the second response contains
Good guess, but nope. Try <b><script>alert(1)</script></b>. - Fixed (10.1.56): the second response contains the default hint (e.g.,
a number next time) and no attacker-controlled markup.
Evidence
bundle/logs/reproduction_steps.log— full script execution log.bundle/repro/vulnerable_resp2.html— HTTP response from the vulnerable Tomcat 10.1.55 showing the unescaped payload.bundle/repro/fixed_resp2.html— HTTP response from the fixed Tomcat 10.1.56 showing the payload is absent.bundle/repro/runtime_manifest.json— runtime evidence manifest.
Key excerpt from vulnerable_resp2.html:
Good guess, but nope. Try <b><script>alert(1)</script></b>.
Key excerpt from fixed_resp2.html:
Good guess, but nope. Try <b>a number next time</b>.
Environment:
- OpenJDK 25.0.3 (installed via
default-jdkpackage) - Apache Tomcat 10.1.55 and 10.1.56 binary distributions from
https://archive.apache.org/dist/tomcat/
Recommendations / Next Steps
- Upgrade to a fixed Tomcat version (10.1.56+, 9.0.119+, or 11.0.23+).
- Remove or disable the
examplesweb application in production environments; it is intended for demonstration only. - Avoid wildcard bean property binding (
property="*") when the request can contain untrusted parameters; explicitly whitelist the parameters the application intends to bind. - Escape output when rendering dynamic content, or use a framework that escapes expression-language output by default.
- Add regression tests that verify untrusted request parameters are not bound into session beans and are not reflected unescaped in responses.
Additional Notes
- The reproduction script is idempotent: it reuses cached Tomcat archives, reconfigures ports deterministically, and cleanly stops each instance before starting the next one.
- The script was run twice consecutively and produced the same confirmed result both times.
- The examples application is typically not deployed in production, which limits real-world exploitability, but the vulnerability is real in the shipped product.
Variant Analysis & Alternative Triggers for CVE-2026-50229
No bypass or distinct alternate trigger was found for CVE-2026-50229. The original Apache fix in webapps/examples/jsp/num/numguess.jsp replaces the wildcard bean property binding (<jsp:setProperty name="numguess" property="*"/>) with an explicit binding to only the guess parameter. This prevents attacker-controlled request parameters from reaching the setHint() setter and, consequently, the unescaped <%= numguess.getHint() %> sink. A systematic search of the bundled examples web application found two other JSPs that still use wildcard binding (colors/colrs.jsp and sessions/carts.jsp), but source inspection and runtime testing on the fixed Tomcat 10.1.56 release showed that neither renders attacker-controlled string properties unescaped. Therefore, the fixed version does not reproduce the original XSS and no new variant was confirmed.
Fix Coverage / Assumptions
The original fix relies on the following invariants:
- Only the
guessparameter is intended to influence theNumberGuessBeanfrom the request. - The
hintproperty is internal and should never be populated from an untrusted request parameter. - Because
setGuess()only ever assigns one of the safe stringshigher,lower, ora number next time, the unescapedgetHint()output is safe once the wildcard binding is removed.
The fix explicitly covers the code path in webapps/examples/jsp/num/numguess.jsp where the JSP container binds request parameters to the session bean. It does not modify any other example JSP, nor does it add output escaping to the getHint() rendering.
Variant / Alternate Trigger
Three candidate entry points were examined in the bundled examples web application:
- Original entry point (tested for regression):
GET /examples/jsp/num/numguess.jsp?hint=<script>alert(1)</script>— no longer reproduces on the fixed version. - Candidate A:
GET /examples/jsp/colors/colrs.jsp?color1=...&color2=...— still uses wildcard binding (<jsp:setProperty name="cb" property="*" />). TheColorGameBeanexposescolor1andcolor2setters, but the JSP rendersgetColor1()andgetColor2(), which return the constrainedforegroundandbackgroundfields.processRequest()only accepts the literal valuesblackorcyanfor those fields. Arbitrary payloads stored incolor1/color2are not reflected. - Candidate B:
GET /examples/jsp/sessions/carts.jsp?itemId=...&submit=...— still uses wildcard binding onsessions.DummyCart. Thesubmitproperty is not reflected anywhere; theitemIdis used to select anItemenum value whose title is rendered throughutil.HTMLFilter.filter().
None of the candidate paths produced unescaped attacker-controlled markup on the fixed release.
- Product / Component: Apache Tomcat bundled
examplesweb application (webapps/examples/jsp/num/numguess.jsp,colors/colrs.jsp,sessions/carts.jsp). - Versions tested: 10.1.55 (vulnerable) and 10.1.56 (fixed) binary distributions from
https://archive.apache.org/dist/tomcat/. - Risk level / consequences: The original CVE is a medium/low-severity XSS in the examples application. No additional or bypassed XSS entry point was confirmed, so the effective risk is unchanged by this variant analysis.
Impact Parity
- Disclosed / claimed maximum impact: Reflected/stored XSS in the bundled Tomcat examples web application via attacker-controlled request parameters.
- Reproduced impact from this variant run: The original
numguess.jspXSS was reproduced on Tomcat 10.1.55 and was absent on Tomcat 10.1.56. The two alternate JSP candidates did not render unescaped attacker-controlled markup on the fixed version. - Parity:
nonefor any variant; the original claim parity isfullon the vulnerable version but not a bypass. - Not demonstrated: A distinct bypass or alternate XSS trigger on the fixed version.
Root Cause
The original root cause is the combination of wildcard JavaBean property binding and unescaped scriptlet output in the same JSP:
<jsp:setProperty name="numguess" property="*"/>
...
Good guess, but nope. Try <b><%= numguess.getHint() %></b>.
The fix removes the wildcard binding, so the attacker can no longer populate the hint property. The same underlying pattern (wildcard binding + unescaped output) exists in two other JSPs, but those beans validate or filter the rendered values, preventing the same XSS outcome.
Fix commit (10.1 branch): 0d5bdd5b0dd964e9f73e530b7d753462b9bfd1d0 — "Minor optimisation. Only need to set 1 property so don't use wild card."
Reproduction Steps
- Run
bundle/vuln_variant/reproduction_steps.sh. - The script reuses the cached Apache Tomcat 10.1.55 and 10.1.56 binary distributions (or downloads them if missing), starts each instance in turn on port
18080, and performs the following checks:- Original
numguess.jspXSS on the vulnerable version (?hint=<script>alert(1)</script>). - Original
numguess.jspXSS on the fixed version. colrs.jspwildcard-binding candidate on the fixed version (?color1=black"><script>alert(1)</script>&color2=cyan).carts.jspwildcard-binding candidate on the fixed version (?itemId=0&submit=<script>alert(1)</script>).
- Original
- Expected evidence:
- Vulnerable
numguess.jspresponse contains the literal<script>alert(1)</script>. - Fixed
numguess.jspresponse does not contain the payload and shows the internal hint. colrs.jspandcarts.jspresponses do not contain the literal payload.
- Vulnerable
Evidence
bundle/logs/vuln_variant.log— full script execution log from both runs.bundle/logs/variant_vulnerable_numguess_*.log/bundle/vuln_variant/vulnerable_numguess_resp2.html— original CVE reproduced on 10.1.55.bundle/logs/variant_fixed_numguess_*.log/bundle/vuln_variant/fixed_numguess_resp2.html— original payload absent on 10.1.56.bundle/logs/variant_fixed_colors_*.log/bundle/vuln_variant/fixed_colors_resp1.html—colrs.jspcandidate did not reflect payload.bundle/logs/variant_fixed_carts_*.log/bundle/vuln_variant/fixed_carts_resp1.html—carts.jspcandidate did not reflect payload.
Key excerpts from the fixed numguess.jsp response:
Good guess, but nope. Try <b>a number next time</b>.
Key excerpt from the colrs.jsp candidate response:
<body bgcolor=cyan>
<font size=6 color=yellow>
The attacker-supplied quote/script string was stored in the bean but never reached the rendered HTML.
Environment:
- OpenJDK 25.0.3 (via
default-jdk) - Apache Tomcat 10.1.55 and 10.1.56 binary distributions
Recommendations / Next Steps
- Confirm the fix is sufficient for the original CVE: it is. The
numguess.jspchange correctly blocks the attacker-controlledhintpath. - Harden the remaining wildcard-binding JSPs (
colrs.jsp,carts.jsp) by switching to explicit property binding, even though they are not currently exploitable. This removes the latent risk that a future bean change reintroduces the same vulnerability pattern. - Add output escaping in
numguess.jspas defense-in-depth, so that even if a new binding path is introduced later, the sink cannot render raw HTML/JavaScript. - Review the examples webapp for any other JSPs that combine
<jsp:setProperty property="*" />with unescaped<%= ... %>scriptlet output.
Additional Notes
- The reproduction script is idempotent: it stops any running Tomcat instances, clears the
work/logs/tempdirectories, and starts each version with a fresh cookie jar. - The script was run twice consecutively and produced the same negative variant result both times.
- The Apache Tomcat security model accepts reports for vulnerabilities in bundled web applications, so the
exampleswebapp is within scope. Administrative users and user-deployed applications are out of scope, which does not affect this finding. - Source inspection of the fixed release confirms that
webapps/examples/jsp/num/numguess.jspcontains<jsp:setProperty name="numguess" property="guess"/>, and the compiled servlet usesJspRuntimeLibrary.introspecthelper(..., "guess", request.getParameter("guess"), request, "guess", false).
CVE-2026-50229 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-50229
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-50229
FAQ: CVE-2026-50229
How does the CVE-2026-50229 XSS attack work?
Which Apache Tomcat versions are affected by CVE-2026-50229, and where is it fixed?
How severe is CVE-2026-50229?
How can I reproduce CVE-2026-50229?
References for CVE-2026-50229
Authoritative sources for CVE-2026-50229 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.