Vulnerabilities in pkg:maven/com.github.jknack/handlebars-springmvc

PACKAGE
Advisory Identifier: GHSA-g29j-rwfv-h99w
Advisory Summary

CVE-2026-63490 is a HIGH (CVSS 7.5) arbitrary file read vulnerability (CWE-552: Files or Directories Accessible to External Parties) affecting Spring Framework libraries in the Maven ecosystem. The issue involves arbitrary file read in `springtemplateloader` via url-fragment suffix bypass. Remediation is available by upgrading affected packages to version 4.5.3.

1 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:maven/com.github.jknack/handlebars-springmvc

1 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:maven/com.github.jknack/handlebars-springmvc. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-g29j-rwfv-h99w CVE-2026-63490 HIGH Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass

CVE-2026-63490 at a Glance

CVE Identifier CVE-2026-63490
GHSA Identifier GHSA-g29j-rwfv-h99w
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-552: Files or Directories Accessible to External Parties
Affected Product Spring Framework
Package Ecosystem Maven
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-63490

To remediate this vulnerability, upgrade all affected Spring Framework dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

com.github.jknack:handlebars-springmvc fixed: 4.5.3
<dependency>
    <groupId>com.github.jknack</groupId>
    <artifactId>handlebars-springmvc</artifactId>
    <version>4.5.3</version>
</dependency>
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

mvn dependency:tree -Dincludes=com.github.jknack

How to Check if You Are Affected

To determine whether your Java application is affected by CVE-2026-63490, inspect your build dependencies using the Maven Dependency Plugin:

mvn dependency:tree -Dincludes=com.github.jknack

Analyze the terminal output to locate any version of Spring Framework in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.5.3), your application is affected.

Note: The mvn dependency:tree -Dincludes=com.github.jknack command inspects the resolved dependency graph declared in your project's POM files; it does not perform automated security scanning or inspect runtime dynamic classpaths. Always verify runtime container classpaths and shaded JARs.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-552: Files or Directories Accessible to External Parties, where the product makes files or directories accessible to unauthorized actors, even though they should not be. In Spring Framework, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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 ```java // 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** ```java // 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`: ```java // 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 ```java // 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 configure(final HandlebarsView view) throws IOException { String url = view.getUrl(); url = url.substring(getPrefix().length(), url.length() - getSuffix().length()); try { view.setTemplate(handlebars.compile(url)); // ← attacker-controlled url view.setValueResolver(valueResolvers.toArray(new ValueResolver[0])); } catch (IOException ex) { if (failOnMissingFile) throw ex; logger.debug("File not found: " + url); } return view; } ``` #### `AbstractTemplateLoader.resolve` - the load-bearing `.hbs` gate ```java // handlebars/.../io/AbstractTemplateLoader.java:47-50 @Override public String resolve(final String uri) { return prefix + normalize(uri) + suffix; // "/" + path + ".hbs" } ``` The suffix string is concatenated as a string. Whether that string lands in the path component, query component, or fragment component of the resulting URL is decided by Spring's URL parsing - not by Handlebars. ### Impact #### Direct primitive Unauthenticated arbitrary file read of any file readable by the JVM process UID. #### Real-world attack chains (downstream impact) 1. **Read `application.yml` -> extract `jwt.secret` / `spring.datasource.password` -> forge admin JWT or directly connect to the database.** Common Spring Boot deployment pattern; one request to game-over. 2. **Read AWS / GCP credentials -> assume role -> exfiltrate buckets, modify infrastructure.** 3. **Read K8s service-account token -> API-server access scoped to the pod's role -> namespace lateral movement, secret exfiltration.** 4. **Read `/proc/self/environ` -> harvest CI/CD-injected secrets that never appear on disk.** 5. **Read private keys (`id_rsa`, TLS keys) -> impersonate host / decrypt MITM'd traffic / sign commits.** 6. **Read the application's source-code-on-disk** to discover further server-side endpoints, hardcoded credentials, or chains. #### Indirect * Confirmed reachable from any controller that returns user-influenced view names - a documented Spring anti-pattern that nevertheless appears in production (CMS preview endpoints, theme switchers, multi-tenant view routing, `@RequestMapping("/{view}")` patterns, `DefaultRequestToViewNameTranslator`-driven URL->view mappings). * No authentication, no privilege, no clicks - a single Internet HTTP GET. ### Remediation Any of the following independently closes the bypass. We recommend implementing **#1 and #2** for defense in depth. #### Apply the containment helper to `SpringTemplateLoader.getResource` (parity with d177cdee) ```java // handlebars-springmvc/.../SpringTemplateLoader.java @Override protected URL getResource(final String location) throws IOException { // For classpath: locations, delegate to the hardened helper as ClassPathTemplateLoader does. // For file: locations, perform an explicit canonical-path containment check. Resource resource = loader.getResource(location); if (!resource.exists()) { return null; } URL url = resource.getURL(); validateNoUnsafeUrlComponents(url); // see 9.3 return url; } ``` #### Validate the resolved URL components ```java private static void validateNoUnsafeUrlComponents(URL url) { if (url.getRef() != null) { throw new IllegalArgumentException( "Template URL must not contain a fragment: " + url); } if (url.getQuery() != null) { throw new IllegalArgumentException( "Template URL must not contain a query: " + url); } } ``` This is the **structural** fix - it ensures the textual `.hbs` check matches the resolved-file behavior regardless of input shape. #### Remove the protocol short-circuit entirely If the supported deployment model is "templates live in one well-known prefix", `SpringTemplateLoader.resolve` should not preserve `file:` / `classpath:` prefixes from user input at all. Either remove that branch, or require an explicit allow-list in the constructor: ```java public SpringTemplateLoader(ResourceLoader loader, boolean allowProtocolPrefixes) { ... } ``` with the default being `false`. #### Validate the stripped view name in `HandlebarsViewResolver.configure` ```java // handlebars-springmvc/.../HandlebarsViewResolver.java:163-178 protected AbstractUrlBasedView configure(final HandlebarsView view) throws IOException { String url = view.getUrl(); url = url.substring(getPrefix().length(), url.length() - getSuffix().length()); if (url.contains(":") || url.contains("#") || url.contains("..")) { throw new IllegalArgumentException("Unsafe view name: " + url); } // ... } ``` This is a defense-in-depth check that rejects view names containing protocols, fragments, or traversal sequences. It does not by itself remove the SpringTemplateLoader weakness (developers calling `handlebars.compile(...)` directly still bypass it), but it eliminates the most common reach pattern.

CVE-2026-63490 Affected Spring Framework Versions

1 component(s)
Fixed Versions:
4.5.3
Product / Package Ecosystem Affected Versions Fixed Version
com.github.jknack:handlebars-springmvc
Maven
< 4.5.3
4.5.3

References

Frequently Asked Questions

What is CVE-2026-63490?
CVE-2026-63490 is a security vulnerability in Spring Framework involving Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass under CWE-552: Files or Directories Accessible to External Parties.
Which Spring Framework versions are affected?
Affected versions include releases prior to 4.5.3 across affected packages: com.github.jknack:handlebars-springmvc.
What version fixes CVE-2026-63490?
Upgrading to 4.5.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is handlebars-springmvc affected?
Yes, com.github.jknack:handlebars-springmvc is affected in releases prior to 4.5.3. Upgrade your dependency to 4.5.3 or later.
What is the CVSS score of CVE-2026-63490?
CVE-2026-63490 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N.
What CWE is associated with CVE-2026-63490?
CVE-2026-63490 is classified under CWE-552: Files or Directories Accessible to External Parties (The product makes files or directories accessible to unauthorized actors, even though they should not be.).
How can I check Maven dependencies for CVE-2026-63490?
Run the dependency tree command: 'mvn dependency:tree -Dincludes=com.github.jknack'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-g29j-rwfv-h99w",
  "modified" : "2026-09-02T22:12:03Z",
  "published" : "2026-09-02T22:12:03Z",
  "aliases" : [ "CVE-2026-63490" ],
  "summary" : "Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass",
  "details" : "### Summary\n`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`).\n\nThe 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`.\n\n**This boundary is bypassed by a single character: `#` (the URL fragment delimiter).**\n\nWhen 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.\n\nResult: **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.\n\n### Vulnerable Code\n\n#### `SpringTemplateLoader.resolve` - preserves `file:` / `classpath:` and applies suffix to the path portion\n\n```java\n// handlebars-springmvc/.../SpringTemplateLoader.java:66-77\n@Override\npublic String resolve(final String location) {\n  String protocol = null;\n  if (location.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {\n    protocol = ResourceUtils.CLASSPATH_URL_PREFIX;\n  } else if (location.startsWith(ResourceUtils.FILE_URL_PREFIX)) {\n    protocol = ResourceUtils.FILE_URL_PREFIX;          // matches \"file:\"\n  }\n  if (protocol == null) {\n    return super.resolve(location);\n  }\n  return protocol + super.resolve(location.substring(protocol.length()));\n}\n```\n\n#### `SpringTemplateLoader.getResource` - **no containment check**\n\n```java\n// handlebars-springmvc/.../SpringTemplateLoader.java:57-63\n@Override\nprotected URL getResource(final String location) throws IOException {\n  Resource resource = loader.getResource(location);   // trust Spring blindly\n  if (!resource.exists()) {\n    return null;\n  }\n  return resource.getURL();\n}\n```\n\nContrast with the hardened sibling `ClassPathTemplateLoader.getResource`, which delegates to `URLTemplateLoader.classpathResource(...)` - the containment helper added by commit `d177cdee`:\n\n```java\n// handlebars/.../io/URLTemplateLoader.java:75-93   (the d177cdee hardening)\nprotected final String classpathResource(String location) {\n  String resolvedPath =\n      Paths.get(location).normalize().toString().replace(java.io.File.separatorChar, '/');\n  if (location.startsWith(\"/\") && !resolvedPath.startsWith(\"/\")) {\n    resolvedPath = \"/\" + resolvedPath;\n  }\n  String prefix = getPrefix();\n  if (!prefix.equals(\"/\") && !resolvedPath.startsWith(prefix)) {\n    throw new IllegalArgumentException(\n        \"Path traversal attempt detected. Resolved path escapes base prefix: \" + location);\n  }\n  return resolvedPath;\n}\n```\n\n`SpringTemplateLoader.getResource` never calls this helper.\n\n#### `HandlebarsViewResolver` - strips the outer prefix/suffix and forwards to compile, no validation\n\n```java\n// handlebars-springmvc/.../HandlebarsViewResolver.java:112-117\npublic HandlebarsViewResolver(final Class<? extends HandlebarsView> viewClass) {\n  setViewClass(viewClass);\n  setContentType(DEFAULT_CONTENT_TYPE);\n  setPrefix(TemplateLoader.DEFAULT_PREFIX);   // \"/\"\n  setSuffix(TemplateLoader.DEFAULT_SUFFIX);   // \".hbs\"\n}\n\n// handlebars-springmvc/.../HandlebarsViewResolver.java:163-178\nprotected AbstractUrlBasedView configure(final HandlebarsView view) throws IOException {\n  String url = view.getUrl();\n  url = url.substring(getPrefix().length(), url.length() - getSuffix().length());\n  try {\n    view.setTemplate(handlebars.compile(url));      // ← attacker-controlled url\n    view.setValueResolver(valueResolvers.toArray(new ValueResolver[0]));\n  } catch (IOException ex) {\n    if (failOnMissingFile) throw ex;\n    logger.debug(\"File not found: \" + url);\n  }\n  return view;\n}\n```\n\n#### `AbstractTemplateLoader.resolve` - the load-bearing `.hbs` gate\n\n```java\n// handlebars/.../io/AbstractTemplateLoader.java:47-50\n@Override\npublic String resolve(final String uri) {\n  return prefix + normalize(uri) + suffix;   // \"/\" + path + \".hbs\"\n}\n```\n\nThe suffix string is concatenated as a string. Whether that string lands in the path component, query component, or fragment component of the resulting URL is decided by Spring's URL parsing - not by Handlebars.\n\n### Impact\n\n#### Direct primitive\n\nUnauthenticated arbitrary file read of any file readable by the JVM process UID.\n\n#### Real-world attack chains (downstream impact)\n\n1. **Read `application.yml` -> extract `jwt.secret` / `spring.datasource.password` -> forge admin JWT or directly connect to the database.** Common Spring Boot deployment pattern; one request to game-over.\n2. **Read AWS / GCP credentials -> assume role -> exfiltrate buckets, modify infrastructure.**\n3. **Read K8s service-account token -> API-server access scoped to the pod's role -> namespace lateral movement, secret exfiltration.**\n4. **Read `/proc/self/environ` -> harvest CI/CD-injected secrets that never appear on disk.**\n5. **Read private keys (`id_rsa`, TLS keys) -> impersonate host / decrypt MITM'd traffic / sign commits.**\n6. **Read the application's source-code-on-disk** to discover further server-side endpoints, hardcoded credentials, or chains.\n\n#### Indirect\n\n* Confirmed reachable from any controller that returns user-influenced view names - a documented Spring anti-pattern that nevertheless appears in production (CMS preview endpoints, theme switchers, multi-tenant view routing, `@RequestMapping(\"/{view}\")` patterns, `DefaultRequestToViewNameTranslator`-driven URL->view mappings).\n* No authentication, no privilege, no clicks - a single Internet HTTP GET.\n\n### Remediation\n\nAny of the following independently closes the bypass. We recommend implementing **#1 and #2** for defense in depth.\n\n#### Apply the containment helper to `SpringTemplateLoader.getResource` (parity with d177cdee)\n\n```java\n// handlebars-springmvc/.../SpringTemplateLoader.java\n@Override\nprotected URL getResource(final String location) throws IOException {\n  // For classpath: locations, delegate to the hardened helper as ClassPathTemplateLoader does.\n  // For file: locations, perform an explicit canonical-path containment check.\n  Resource resource = loader.getResource(location);\n  if (!resource.exists()) {\n    return null;\n  }\n  URL url = resource.getURL();\n  validateNoUnsafeUrlComponents(url);   // see 9.3\n  return url;\n}\n```\n\n#### Validate the resolved URL components\n\n```java\nprivate static void validateNoUnsafeUrlComponents(URL url) {\n  if (url.getRef() != null) {\n    throw new IllegalArgumentException(\n        \"Template URL must not contain a fragment: \" + url);\n  }\n  if (url.getQuery() != null) {\n    throw new IllegalArgumentException(\n        \"Template URL must not contain a query: \" + url);\n  }\n}\n```\n\nThis is the **structural** fix - it ensures the textual `.hbs` check matches the resolved-file behavior regardless of input shape.\n\n#### Remove the protocol short-circuit entirely\n\nIf the supported deployment model is \"templates live in one well-known prefix\", `SpringTemplateLoader.resolve` should not preserve `file:` / `classpath:` prefixes from user input at all. Either remove that branch, or require an explicit allow-list in the constructor:\n\n```java\npublic SpringTemplateLoader(ResourceLoader loader, boolean allowProtocolPrefixes) { ... }\n```\n\nwith the default being `false`.\n\n#### Validate the stripped view name in `HandlebarsViewResolver.configure`\n\n```java\n// handlebars-springmvc/.../HandlebarsViewResolver.java:163-178\nprotected AbstractUrlBasedView configure(final HandlebarsView view) throws IOException {\n  String url = view.getUrl();\n  url = url.substring(getPrefix().length(), url.length() - getSuffix().length());\n  if (url.contains(\":\") || url.contains(\"#\") || url.contains(\"..\")) {\n    throw new IllegalArgumentException(\"Unsafe view name: \" + url);\n  }\n  // ...\n}\n```\n\nThis is a defense-in-depth check that rejects view names containing protocols, fragments, or traversal sequences. It does not by itself remove the SpringTemplateLoader weakness (developers calling `handlebars.compile(...)` directly still bypass it), but it eliminates the most common reach pattern.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Maven",
      "name" : "com.github.jknack:handlebars-springmvc"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "4.5.3"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/jknack/handlebars.java/security/advisories/GHSA-g29j-rwfv-h99w"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-63490"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/jknack/handlebars.java/commit/61f43423a337b87db5fec1fe59f0725aaaa38df5"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/jknack/handlebars.java"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/jknack/handlebars.java/releases/tag/v4.5.3"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-22", "CWE-23", "CWE-552" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-02T22:12:03Z",
    "nvd_published_at" : "2026-08-20T15:18:04Z"
  }
}