google / google/closure-compiler

Arbitrary file read via //# sourceMappingURL= path traversal in SourceMapResolver

Open
#4,322 2 comments 1 reaction 2 assignees Claimed by @kunaaaalcodes View on GitHub
Dominant language
JavaScript
Stars
7.7k
Forks
1.2k
Avg merge
2d 12h
Merged PRs (30d)
6

Description

## 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](https://github.com/advisories/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

1. Attacker supplies a JS file containing:
```
//# sourceMappingURL=../../../../etc/passwd
```
2. The parser extracts `sourceMapURL = "../../../../etc/passwd"`.
3. `SourceMapResolver.extractSourceMap(jsFile, "../../../../etc/passwd", parseInline)` is invoked.
4. `isAbsolute("../../../../etc/passwd")` → `false` (it's a relative path — not blocked).
5. `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).
6. Returns a `SourceFile` built from that resolved path.
7. `compiler.addInputSourceMap(jsFileName, new SourceMapInput(sourceMapSourceFile))` registers it.
8. Later, `inputSourceMap.getSourceMap(errorManager)` calls `sourceFile.getCode()`, which reads the file's contents from disk.
9. 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.