Summary
closure-compiler reads arbitrary files from the local filesystem when compiling JavaScript source that contains a crafted //# sourceMappingURL= comment with a relative path-traversal sequence. No bounds check is applied to keep the resolved path within the expected directory.
This was reported to the Google OSS VRP (bughunters.google.com). The team closed it as Infeasible (does not meet OSS VRP's bar, since it requires closure-compiler to already be compiling untrusted/third-party source) and explicitly authorized public disclosure here. Filing per that guidance so the pattern is documented and users who compile untrusted input can mitigate it.
This is the same bug class as CVE-2026-49356 / GHSA-4x5r-pxfx-6jf8 in @babel/core (fixed June 13, 2026) — an inline sourceMappingURL comment used to read arbitrary files via unbounded relative path resolution. closure-compiler has an independently vulnerable implementation of the same pattern.
Root cause
src/com/google/javascript/jscomp/SourceMapResolver.java:
private static boolean isAbsolute(String url) {
try {
return new URI(url).isAbsolute() || url.startsWith("/");
} ...
}
static @Nullable SourceFile getRelativePath(String baseFilePath, String relativePath) {
return SourceFile.builder()
.withPath(
FileSystems.getDefault().getPath(baseFilePath).resolveSibling(relativePath).normalize())
...
}
isAbsolute() only rejects URIs that are themselves absolute (file:///etc/passwd) or start with /. It does not reject relative traversal sequences like ../../../../etc/passwd, so those pass through to getRelativePath(). There, resolveSibling(relativePath).normalize() resolves the traversal but the result is never checked against the base directory (no resolved.startsWith(base) guard anywhere in this function).
Full attack chain
- Attacker supplies a JS file containing:
//# sourceMappingURL=../../../../etc/passwd
- The parser extracts
sourceMapURL = "../../../../etc/passwd".
SourceMapResolver.extractSourceMap(jsFile, "../../../../etc/passwd", parseInline) is invoked.
isAbsolute("../../../../etc/passwd") → false (it's a relative path — not blocked).
getRelativePath(jsFile.getName(), "../../../../etc/passwd") → e.g. Path.of("input.js").resolveSibling("../../../../etc/passwd").normalize() → resolves outside the working directory (e.g. /etc/passwd when the base path allows enough .. segments, or further up the tree relative to CWD otherwise).
- Returns a
SourceFile built from that resolved path.
compiler.addInputSourceMap(jsFileName, new SourceMapInput(sourceMapSourceFile)) registers it.
- Later,
inputSourceMap.getSourceMap(errorManager) calls sourceFile.getCode(), which reads the file's contents from disk.
- The file contents surface to the attacker via compiler error messages (e.g.
SOURCEMAP_PARSE_FAILED when the target file isn't valid source-map JSON, which will include a snippet/parse context) and/or via the sourcesContent array of the compiler's own output source map, if source map output is enabled.
No special flag is required — resolveSourceMapAnnotations (which governs whether sourceMappingURL comments are honored) defaults to true.
Affected scenarios
Impactful wherever closure-compiler processes JavaScript it doesn't fully control:
- SaaS/hosted build or minification services accepting user-submitted JS
- CI/CD pipelines compiling third-party or unreviewed dependencies
- Monorepos where one tenant's/team's code is compiled in a shared build environment
- IDE or dev-tool integrations that run closure-compiler over arbitrary opened files
Users who only ever compile their own fully-trusted source are not affected.
Suggested fix
Add a bounds check in getRelativePath() so the resolved path can't escape the expected base directory:
static @Nullable SourceFile getRelativePath(String baseFilePath, String relativePath) {
Path base = FileSystems.getDefault().getPath(baseFilePath).toAbsolutePath().normalize();
Path resolved = base.resolveSibling(relativePath).toAbsolutePath().normalize();
if (!resolved.startsWith(base.getParent())) {
return null; // reject traversal outside the base directory
}
return SourceFile.builder()
.withPath(resolved)
.withKind(SourceKind.NON_CODE)
.build();
}
An alternative, lower-effort mitigation (matching Babel's approach) would be to default resolveSourceMapAnnotations to false, requiring an explicit opt-in for any environment that needs inline source-map resolution over trusted input.
Comparison with CVE-2026-49356 (@babel/core)
|
@babel/core (CVE-2026-49356, patched) |
closure-compiler (this issue) |
| Trigger |
//# sourceMappingURL= comment |
//# sourceMappingURL= comment |
| Enabled by default |
Yes |
Yes (resolveSourceMapAnnotations=true) |
| Bounds check before fix |
None |
None |
| Disclosure vector |
Output source map |
Compiler error messages + output sourcesContent |
| Status |
Fixed June 13, 2026 |
Unpatched as of this report |
Verification
Dynamically verified against closure-compiler v20260629 (latest at time of testing) on Java 21, by compiling a JS input containing a sourceMappingURL comment pointing outside the working directory and observing the target file's contents surface via compiler error output.
Environment
- Reported to bughunters.google.com; closed as Infeasible for OSS VRP purposes, with explicit permission granted to disclose publicly here.
SourceMapResolver.java last touched by a refactor-only commit (no security-relevant change) prior to this report; pattern confirmed still present as of the date of this issue.
Summary
closure-compilerreads arbitrary files from the local filesystem when compiling JavaScript source that contains a crafted//# sourceMappingURL=comment with a relative path-traversal sequence. No bounds check is applied to keep the resolved path within the expected directory.This was reported to the Google OSS VRP (bughunters.google.com). The team closed it as Infeasible (does not meet OSS VRP's bar, since it requires closure-compiler to already be compiling untrusted/third-party source) and explicitly authorized public disclosure here. Filing per that guidance so the pattern is documented and users who compile untrusted input can mitigate it.
This is the same bug class as CVE-2026-49356 / GHSA-4x5r-pxfx-6jf8 in
@babel/core(fixed June 13, 2026) — an inlinesourceMappingURLcomment used to read arbitrary files via unbounded relative path resolution. closure-compiler has an independently vulnerable implementation of the same pattern.Root cause
src/com/google/javascript/jscomp/SourceMapResolver.java:isAbsolute()only rejects URIs that are themselves absolute (file:///etc/passwd) or start with/. It does not reject relative traversal sequences like../../../../etc/passwd, so those pass through togetRelativePath(). There,resolveSibling(relativePath).normalize()resolves the traversal but the result is never checked against the base directory (noresolved.startsWith(base)guard anywhere in this function).Full attack chain
sourceMapURL = "../../../../etc/passwd".SourceMapResolver.extractSourceMap(jsFile, "../../../../etc/passwd", parseInline)is invoked.isAbsolute("../../../../etc/passwd")→false(it's a relative path — not blocked).getRelativePath(jsFile.getName(), "../../../../etc/passwd")→ e.g.Path.of("input.js").resolveSibling("../../../../etc/passwd").normalize()→ resolves outside the working directory (e.g./etc/passwdwhen the base path allows enough..segments, or further up the tree relative to CWD otherwise).SourceFilebuilt from that resolved path.compiler.addInputSourceMap(jsFileName, new SourceMapInput(sourceMapSourceFile))registers it.inputSourceMap.getSourceMap(errorManager)callssourceFile.getCode(), which reads the file's contents from disk.SOURCEMAP_PARSE_FAILEDwhen the target file isn't valid source-map JSON, which will include a snippet/parse context) and/or via thesourcesContentarray of the compiler's own output source map, if source map output is enabled.No special flag is required —
resolveSourceMapAnnotations(which governs whethersourceMappingURLcomments are honored) defaults totrue.Affected scenarios
Impactful wherever closure-compiler processes JavaScript it doesn't fully control:
Users who only ever compile their own fully-trusted source are not affected.
Suggested fix
Add a bounds check in
getRelativePath()so the resolved path can't escape the expected base directory:An alternative, lower-effort mitigation (matching Babel's approach) would be to default
resolveSourceMapAnnotationstofalse, requiring an explicit opt-in for any environment that needs inline source-map resolution over trusted input.Comparison with CVE-2026-49356 (@babel/core)
//# sourceMappingURL=comment//# sourceMappingURL=commentresolveSourceMapAnnotations=true)sourcesContentVerification
Dynamically verified against closure-compiler v20260629 (latest at time of testing) on Java 21, by compiling a JS input containing a
sourceMappingURLcomment pointing outside the working directory and observing the target file's contents surface via compiler error output.Environment
SourceMapResolver.javalast touched by a refactor-only commit (no security-relevant change) prior to this report; pattern confirmed still present as of the date of this issue.