{"id":"CVE-2026-63490","aliases":["EUVD-2026-63392","GHSA-g29j-rwfv-h99w"],"status":"published","title":"Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass","description":"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.","severity":{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00688,"percentile":0.53198,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-22","CWE-23","CWE-552"],"published":"2026-08-20T14:45:35Z","modified":"2026-10-28T11:17:03Z","fixes":[{"ecosystem":"Maven","package":"com.github.jknack:handlebars-springmvc","fixed_in":["4.5.3"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Maven","package":"com.github.jknack:handlebars-springmvc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.5.3"}]}]},{"source":"osv","ecosystem":"Maven","package":"com.github.jknack:handlebars-springmvc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.5.3"}]}],"versions":["0.10.0","0.11.0","0.12.0","0.4.0","0.4.1","0.4.2","0.5.0","0.5.1","0.5.2","0.5.3","0.5.4","0.5.5","0.6.0","0.6.1","0.6.2","0.7.0","0.8.0","0.9.0","1.0.0","1.1.0","1.1.1","1.1.2","1.2.0","1.2.1","1.3.0","1.3.1","1.3.2","2.0.0","2.1.0","2.2.0","2.2.1","2.2.2","2.2.3","2.3.0","2.3.1","2.3.2","3.0.0","4.0.0","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6","4.0.7","4.1.0","4.1.1","4.1.2","4.2.0","4.2.1","4.3.0","4.3.1","4.4.0","4.5.0","4.5.1","4.5.2"]}],"products":[{"vendor":"jknack","product":"handlebars.java"}],"references":[{"url":"https://github.com/jknack/handlebars.java/security/advisories/GHSA-g29j-rwfv-h99w","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/jknack/handlebars.java/commit/61f43423a337b87db5fec1fe59f0725aaaa38df5","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/jknack/handlebars.java/releases/tag/v4.5.3","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-63490","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-g29j-rwfv-h99w"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/63xxx/CVE-2026-63490.json","tags":["advisory"]},{"url":"https://github.com/jknack/handlebars.java","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-63490","title":"Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass","description":"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.","status":"published","published":"2026-08-20T14:45:35.727Z","modified":"2026-08-25T14:41:57.989Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"cna"}],"cwes":["CWE-22","CWE-23","CWE-552"],"updated_at":"2026-11-02T14:19:55.914391Z"}],"euvd":[{"id":"EUVD-2026-63392","description":"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.","status":"published","published":"2026-08-20T14:45:35Z","modified":"2026-08-25T14:41:57Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"updated_at":"2026-11-02T18:01:32.944243Z"}],"ghsa":[{"id":"GHSA-g29j-rwfv-h99w","title":"Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass","description":"### 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(\"/\") \u0026\u0026 !resolvedPath.startsWith(\"/\")) {\n    resolvedPath = \"/\" + resolvedPath;\n  }\n  String prefix = getPrefix();\n  if (!prefix.equals(\"/\") \u0026\u0026 !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\u003c? extends HandlebarsView\u003e 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` -\u003e extract `jwt.secret` / `spring.datasource.password` -\u003e forge admin JWT or directly connect to the database.** Common Spring Boot deployment pattern; one request to game-over.\n2. **Read AWS / GCP credentials -\u003e assume role -\u003e exfiltrate buckets, modify infrastructure.**\n3. **Read K8s service-account token -\u003e API-server access scoped to the pod's role -\u003e namespace lateral movement, secret exfiltration.**\n4. **Read `/proc/self/environ` -\u003e harvest CI/CD-injected secrets that never appear on disk.**\n5. **Read private keys (`id_rsa`, TLS keys) -\u003e 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-\u003eview 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.","status":"published","published":"2026-09-02T22:12:03Z","modified":"2026-09-02T22:12:04Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-22","CWE-23","CWE-552"],"updated_at":"2026-11-02T14:41:11.621873Z"}],"nvd":[{"id":"CVE-2026-63490","description":"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.","status":"published","published":"2026-08-20T15:18:04.577Z","modified":"2026-10-18T20:10:00.11Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"secondary"}],"cwes":["CWE-22","CWE-23","CWE-552"],"updated_at":"2026-11-02T14:12:40.302911Z"}],"osv":[{"id":"CVE-2026-63490","title":"Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass","description":"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.","status":"published","published":"2026-08-20T14:45:35.727Z","modified":"2026-10-28T11:17:03.816742903Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-22","CWE-23","CWE-552"],"updated_at":"2026-11-02T15:45:13.306117Z"},{"id":"GHSA-g29j-rwfv-h99w","title":"Handlebars.java: Arbitrary file read in `SpringTemplateLoader` via URL-fragment suffix bypass","description":"### 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(\"/\") \u0026\u0026 !resolvedPath.startsWith(\"/\")) {\n    resolvedPath = \"/\" + resolvedPath;\n  }\n  String prefix = getPrefix();\n  if (!prefix.equals(\"/\") \u0026\u0026 !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\u003c? extends HandlebarsView\u003e 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` -\u003e extract `jwt.secret` / `spring.datasource.password` -\u003e forge admin JWT or directly connect to the database.** Common Spring Boot deployment pattern; one request to game-over.\n2. **Read AWS / GCP credentials -\u003e assume role -\u003e exfiltrate buckets, modify infrastructure.**\n3. **Read K8s service-account token -\u003e API-server access scoped to the pod's role -\u003e namespace lateral movement, secret exfiltration.**\n4. **Read `/proc/self/environ` -\u003e harvest CI/CD-injected secrets that never appear on disk.**\n5. **Read private keys (`id_rsa`, TLS keys) -\u003e 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-\u003eview 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.","status":"published","published":"2026-09-02T22:12:03Z","modified":"2026-09-02T22:15:05.350759964Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-22","CWE-23","CWE-552"],"updated_at":"2026-11-02T15:46:47.119724Z"}]}}
