CVE-2026-63490
Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass
Handlebars.java provides logic-less and semantic Mustache templates with Java. Prior to 4.5.3, com.github.jknack.handlebars.springmvc.SpringTemplateLoader resolves attacker-influenced Spring MVC view names through Spring ResourceLoader without the path-containment validation used by other URL-based loaders. In handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/SpringTemplateLoader.java, a view name using a file: or classpath: URL and ending with the # fragment delimiter places the appended .hbs suffix in the fragment, which FileUrlResource.exists() and URL.openStream() discard. HandlebarsViewResolver in handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/HandlebarsViewResolver.java then passes the attacker-controlled name to handlebars.compile(), allowing an unauthenticated remote attacker to read files accessible to the JVM when an application exposes a controller with a user-influenced view name. This issue is fixed in version 4.5.3.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
7.5
Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass
Handlebars.java provides logic-less and semantic Mustache templates with Java. Prior to 4.5.3, com.github.jknack.handlebars.springmvc.SpringTemplateLoader resolves attacker-influenced Spring MVC view names through Spring ResourceLoader without the path-containment validation used by other URL-based loaders. In handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/SpringTemplateLoader.java, a view name using a file: or classpath: URL and ending with the # fragment delimiter places the appended .hbs suffix in the fragment, which FileUrlResource.exists() and URL.openStream() discard. HandlebarsViewResolver in handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/HandlebarsViewResolver.java then passes the attacker-controlled name to handlebars.compile(), allowing an unauthenticated remote attacker to read files accessible to the JVM when an application exposes a controller with a user-influenced view name. This issue is fixed in version 4.5.3.
- Record
CVE-2026-63490- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-22, CWE-23, CWE-552
- Updated
- 2026-11-02
EUVDEUVD-2026-63392
7.5
Handlebars.java provides logic-less and semantic Mustache templates with Java. Prior to 4.5.3, com.github.jknack.handlebars.springmvc.SpringTemplateLoader resolves attacker-influenced Spring MVC view names through Spring ResourceLoader without the path-containment validation used by other URL-based loaders. In handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/SpringTemplateLoader.java, a view name using a file: or classpath: URL and ending with the # fragment delimiter places the appended .hbs suffix in the fragment, which FileUrlResource.exists() and URL.openStream() discard. HandlebarsViewResolver in handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/HandlebarsViewResolver.java then passes the attacker-controlled name to handlebars.compile(), allowing an unauthenticated remote attacker to read files accessible to the JVM when an application exposes a controller with a user-influenced view name. This issue is fixed in version 4.5.3.
- Record
EUVD-2026-63392- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-g29j-rwfv-h99w
7.5
Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass
Summary
com.github.jknack.handlebars.springmvc.SpringTemplateLoader resolves Spring MVC view names into URLs via Spring's ResourceLoader without applying the path-containment check that protects every other URL-based loader in the project (ClassPathTemplateLoader, FileTemplateLoader, ServletContextTemplateLoader - all hardened by commit d177cdee).
The only remaining defense for file: / classpath: view names is the unconditional .hbs suffix appended by AbstractTemplateLoader.resolve(...). This suffix is the load-bearing security boundary that prevents a request like view=file:/etc/passwd from reading /etc/passwd instead of /etc/passwd.hbs.
This boundary is bypassed by a single character: # (the URL fragment delimiter).
When the view name ends with #, the appended .hbs lands inside the URL fragment. Both Spring's FileUrlResource.exists() (via URI.getSchemeSpecificPart()) and the JDK's URL.openStream() (via URL.getFile()) silently discard the fragment, so the file actually opened is the bare path the attacker specified - for example /etc/passwd rather than /etc/passwd.hbs. The compiled "template" is then parsed and rendered into the HTTP response body.
Result: unauthenticated, network-reachable, arbitrary file read of any file readable by the JVM process on any Spring MVC application that uses a default-configured HandlebarsViewResolver and exposes a controller that returns a (fully or partly) user-influenced view name.
Vulnerable Code
SpringTemplateLoader.resolve - preserves file: / classpath: and applies suffix to the path portion
// handlebars-springmvc/.../SpringTemplateLoader.java:66-77
@Override
public String resolve(final String location) {
String protocol = null;
if (location.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
protocol = ResourceUtils.CLASSPATH_URL_PREFIX;
} else if (location.startsWith(ResourceUtils.FILE_URL_PREFIX)) {
protocol = ResourceUtils.FILE_URL_PREFIX; // matches "file:"
}
if (protocol == null) {
return super.resolve(location);
}
return protocol + super.resolve(location.substring(protocol.length()));
}
SpringTemplateLoader.getResource - no containment check
// handlebars-springmvc/.../SpringTemplateLoader.java:57-63
@Override
protected URL getResource(final String location) throws IOException {
Resource resource = loader.getResource(location); // trust Spring blindly
if (!resource.exists()) {
return null;
}
return resource.getURL();
}
Contrast with the hardened sibling ClassPathTemplateLoader.getResource, which delegates to URLTemplateLoader.classpathResource(...) - the containment helper added by commit d177cdee:
// handlebars/.../io/URLTemplateLoader.java:75-93 (the d177cdee hardening)
protected final String classpathResource(String location) {
String resolvedPath =
Paths.get(location).normalize().toString().replace(java.io.File.separatorChar, '/');
if (location.startsWith("/") && !resolvedPath.startsWith("/")) {
resolvedPath = "/" + resolvedPath;
}
String prefix = getPrefix();
if (!prefix.equals("/") && !resolvedPath.startsWith(prefix)) {
throw new IllegalArgumentException(
"Path traversal attempt detected. Resolved path escapes base prefix: " + location);
}
return resolvedPath;
}
SpringTemplateLoader.getResource never calls this helper.
HandlebarsViewResolver - strips the outer prefix/suffix and forwards to compile, no validation
// handlebars-springmvc/.../HandlebarsViewResolver.java:112-117
public HandlebarsViewResolver(final Class<? extends HandlebarsView> viewClass) {
setViewClass(viewClass);
setContentType(DEFAULT_CONTENT_TYPE);
setPrefix(TemplateLoader.DEFAULT_PREFIX); // "/"
setSuffix(TemplateLoader.DEFAULT_SUFFIX); // ".hbs"
}
// handlebars-springmvc/.../HandlebarsViewResolver.java:163-178
protected AbstractUrlBasedView…
- Record
GHSA-g29j-rwfv-h99w- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-22, CWE-23, CWE-552
- Updated
- 2026-11-02
NVD
7.5
Handlebars.java provides logic-less and semantic Mustache templates with Java. Prior to 4.5.3, com.github.jknack.handlebars.springmvc.SpringTemplateLoader resolves attacker-influenced Spring MVC view names through Spring ResourceLoader without the path-containment validation used by other URL-based loaders. In handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/SpringTemplateLoader.java, a view name using a file: or classpath: URL and ending with the # fragment delimiter places the appended .hbs suffix in the fragment, which FileUrlResource.exists() and URL.openStream() discard. HandlebarsViewResolver in handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/HandlebarsViewResolver.java then passes the attacker-controlled name to handlebars.compile(), allowing an unauthenticated remote attacker to read files accessible to the JVM when an application exposes a controller with a user-influenced view name. This issue is fixed in version 4.5.3.
- Record
CVE-2026-63490- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-22, CWE-23, CWE-552
- Updated
- 2026-11-02
OSV
7.5
Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass
Handlebars.java provides logic-less and semantic Mustache templates with Java. Prior to 4.5.3, com.github.jknack.handlebars.springmvc.SpringTemplateLoader resolves attacker-influenced Spring MVC view names through Spring ResourceLoader without the path-containment validation used by other URL-based loaders. In handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/SpringTemplateLoader.java, a view name using a file: or classpath: URL and ending with the # fragment delimiter places the appended .hbs suffix in the fragment, which FileUrlResource.exists() and URL.openStream() discard. HandlebarsViewResolver in handlebars-springmvc/src/main/java/com/github/jknack/handlebars/springmvc/HandlebarsViewResolver.java then passes the attacker-controlled name to handlebars.compile(), allowing an unauthenticated remote attacker to read files accessible to the JVM when an application exposes a controller with a user-influenced view name. This issue is fixed in version 4.5.3.
- Record
CVE-2026-63490- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-22, CWE-23, CWE-552
- Updated
- 2026-11-02
OSVGHSA-g29j-rwfv-h99w
7.5
Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass
Summary
com.github.jknack.handlebars.springmvc.SpringTemplateLoader resolves Spring MVC view names into URLs via Spring's ResourceLoader without applying the path-containment check that protects every other URL-based loader in the project (ClassPathTemplateLoader, FileTemplateLoader, ServletContextTemplateLoader - all hardened by commit d177cdee).
The only remaining defense for file: / classpath: view names is the unconditional .hbs suffix appended by AbstractTemplateLoader.resolve(...). This suffix is the load-bearing security boundary that prevents a request like view=file:/etc/passwd from reading /etc/passwd instead of /etc/passwd.hbs.
This boundary is bypassed by a single character: # (the URL fragment delimiter).
When the view name ends with #, the appended .hbs lands inside the URL fragment. Both Spring's FileUrlResource.exists() (via URI.getSchemeSpecificPart()) and the JDK's URL.openStream() (via URL.getFile()) silently discard the fragment, so the file actually opened is the bare path the attacker specified - for example /etc/passwd rather than /etc/passwd.hbs. The compiled "template" is then parsed and rendered into the HTTP response body.
Result: unauthenticated, network-reachable, arbitrary file read of any file readable by the JVM process on any Spring MVC application that uses a default-configured HandlebarsViewResolver and exposes a controller that returns a (fully or partly) user-influenced view name.
Vulnerable Code
SpringTemplateLoader.resolve - preserves file: / classpath: and applies suffix to the path portion
// handlebars-springmvc/.../SpringTemplateLoader.java:66-77
@Override
public String resolve(final String location) {
String protocol = null;
if (location.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
protocol = ResourceUtils.CLASSPATH_URL_PREFIX;
} else if (location.startsWith(ResourceUtils.FILE_URL_PREFIX)) {
protocol = ResourceUtils.FILE_URL_PREFIX; // matches "file:"
}
if (protocol == null) {
return super.resolve(location);
}
return protocol + super.resolve(location.substring(protocol.length()));
}
SpringTemplateLoader.getResource - no containment check
// handlebars-springmvc/.../SpringTemplateLoader.java:57-63
@Override
protected URL getResource(final String location) throws IOException {
Resource resource = loader.getResource(location); // trust Spring blindly
if (!resource.exists()) {
return null;
}
return resource.getURL();
}
Contrast with the hardened sibling ClassPathTemplateLoader.getResource, which delegates to URLTemplateLoader.classpathResource(...) - the containment helper added by commit d177cdee:
// handlebars/.../io/URLTemplateLoader.java:75-93 (the d177cdee hardening)
protected final String classpathResource(String location) {
String resolvedPath =
Paths.get(location).normalize().toString().replace(java.io.File.separatorChar, '/');
if (location.startsWith("/") && !resolvedPath.startsWith("/")) {
resolvedPath = "/" + resolvedPath;
}
String prefix = getPrefix();
if (!prefix.equals("/") && !resolvedPath.startsWith(prefix)) {
throw new IllegalArgumentException(
"Path traversal attempt detected. Resolved path escapes base prefix: " + location);
}
return resolvedPath;
}
SpringTemplateLoader.getResource never calls this helper.
HandlebarsViewResolver - strips the outer prefix/suffix and forwards to compile, no validation
// handlebars-springmvc/.../HandlebarsViewResolver.java:112-117
public HandlebarsViewResolver(final Class<? extends HandlebarsView> viewClass) {
setViewClass(viewClass);
setContentType(DEFAULT_CONTENT_TYPE);
setPrefix(TemplateLoader.DEFAULT_PREFIX); // "/"
setSuffix(TemplateLoader.DEFAULT_SUFFIX); // ".hbs"
}
// handlebars-springmvc/.../HandlebarsViewResolver.java:163-178
protected AbstractUrlBasedView…
- Record
GHSA-g29j-rwfv-h99w- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-22, CWE-23, CWE-552
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
com.github.jknack:handlebars-springmvcMaven |
before 4.5.3 | 4.5.3 |
com.github.jknack:handlebars-springmvcMaven |
before 4.5.3, plus listed versions 0.10.00.11.00.12.00.4.00.4.1 Show 45 moreShow less0.4.20.5.00.5.10.5.20.5.30.5.40.5.50.6.00.6.10.6.20.7.00.8.00.9.01.0.01.1.01.1.11.1.21.2.01.2.11.3.01.3.11.3.22.0.02.1.02.2.02.2.12.2.22.2.32.3.02.3.12.3.23.0.04.0.04.0.14.0.24.0.34.0.44.0.54.0.64.0.74.1.04.1.14.1.24.2.04.2.1 |
4.5.3 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-63490 is a high vulnerability (CVSS 7.5) in Jknack Handlebars.java, published August 20, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.69%, the 52nd percentile. Fixed in com.github.jknack:handlebars-springmvc 4.5.3.
References
github.com/jknack/handlebars.java/security/advisories/GHSA-g29j-rwfv-h99w
github.com/jknack/handlebars.java/commit/61f43423a337b87db5fec1fe59f0725aaaa38df5
github.com/jknack/handlebars.java/releases/tag/v4.5.3
nvd.nist.gov/vuln/detail/CVE-2026-63490
github.com/advisories/GHSA-g29j-rwfv-h99w
github.com/CVEProject/cvelistV5/tree/main/cves/2026/63xxx/CVE-2026-63490.json
github.com/jknack/handlebars.java
Left out here: 6 versions and the rest of 2 source descriptions. The JSON record has them.