# Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass

Handlebars.java SpringTemplateLoader appends .hbs to view names, but a trailing '#' puts the suffix in the URL fragment, which is discarded, enabling unauthenticated arbitrary file read.

- Payout: Research
- Severity: high
- Program: Handlebars.java
- Found by: hypnguyen1209
- Disclosed: 2026-09-02
- CVE: CVE-2026-63490
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-g29j-rwfv-h99w
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-g29j-rwfv-h99w
- Tags: area:web domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:java technique:incomplete-fix technique:source-review vuln:info-disclosure vuln:path-traversal

**Target:** Spring MVC view resolver / SpringTemplateLoader (handlebars-springmvc)
**Root cause:** SpringTemplateLoader.getResource delegates to Spring's ResourceLoader without the path-containment check used by sibling loaders; the only defense is the appended .hbs suffix, which lands in the URL fragment when the view name ends with '#', and both FileUrlResource.exists() and URL.openStream() discard the fragment.
**Steps:**
1. Identify a Spring MVC controller returning a user-influenced view name resolved by a default HandlebarsViewResolver.
2. Send a request with view name `file:/etc/passwd#` (trailing '#').
3. AbstractTemplateLoader.resolve appends `.hbs`, producing `file:/etc/passwd#.hbs`; the suffix is inside the fragment.
4. Spring's FileUrlResource.exists() and JDK URL.openStream() strip the fragment, opening `/etc/passwd`.
5. The file content is parsed as a template and rendered into the HTTP response body.
**How found:** Noticed SpringTemplateLoader lacked the d177cdee containment helper its sibling loaders use, leaving the .hbs suffix as the sole boundary; then reasoned about URL component parsing and found that a '#' delimiter relocates the suffix into the fragment, which both Spring and the JDK discard.
**Exploitation:** Single unauthenticated HTTP GET to any endpoint that forwards a user-influenced view name; the trailing '#' moves the mandatory .hbs suffix into the URL fragment, which is silently dropped at open time, so the bare attacker-specified path is read and rendered into the response.
**Escalation:** Read application.yml for jwt.secret/datasource password to forge admin JWTs or hit the DB; read cloud credentials, K8s service-account tokens, /proc/self/environ, or private keys for lateral movement.
**Insight:** When a security check is a string concatenation onto a URL/path (like appending a suffix), test delimiter characters ('#', '?') that move the appended text into a component the consumer ignores; compare hardened sibling classes to spot the one loader missing the shared containment helper.

