Description
This is a security vulnerability report. As per the repo's SECURITY.md, I'm filing it through the issue tracker. The root cause below is detailed enough that a working proof of concept could be reconstructed from it fairly quickly, I'm not claiming otherwise. What I am doing, as standard disclosure practice, is not posting the literal request string I used, so this issue isn't a copy-paste exploit for anyone browsing public issues. See "Notes" for what I'm proposing instead.
Expected behavior: FileSystemCacheHandler should only ever write cache files inside the configured cacheFolderPath`, regardless of what URL was requested.
Actual behavior: on Windows, a specially crafted request path allows FileSystemCacheHandler to write (and overwrite) .html files outside the configured cache directory, including files that are actually served to end users. I've reproduced this in a minimal test app: a crafted request causes the application's live, served entry file to be overwritten, so subsequent normal visitors receive the corrupted content instead of the real application.
Root cause
convertCacheKeyToFileName() in filesystem-cache-handler.ts only sanitizes / and ? in the cache key before it's used to build a filesystem path:
function convertCacheKeyToFileName(cacheKey) {
return cacheKey
.replace(new RegExp('/', 'g'), '__')
.replace(new RegExp('\\?', 'g'), '++');
}
It does not sanitize \. The resulting string is then passed to path.join(cacheFolderPath, '/', fileName). On Windows, path.join treats \ as a path separator and will resolve ..\ traversal sequences, allowing
the final write location to escape cacheFolderPath entirely. The cache key itself is derived from the raw request URL by default, with no additional sanitization.
Steps to Reproduce the Issue
- Create an Angular SSR app using
@rx-angular/isr with FileSystemCacheHandler configured, per the library's documented usage in the README.
- Ensure at least one route has
revalidate set in its route data (required for ISR to cache anything).
- Deploy/run on a Windows host.
- Send a single crafted HTTP GET request to the running server containing a request path with backslash-based directory traversal sequences (
..\) targeting a file outside the cache directory.
- Observe that a
.html file is created/overwritten at the traversed location rather than inside cacheFolderPath.
- If the target overlaps with the app's actually-served static assets, a subsequent normal request to the application shows the corrupted content.
Impact
Verified end-to-end against a minimal Angular SSR app configured per the library's documented usage (FileSystemCacheHandler, revalidate set on the route), on Windows:
1. Escapes the configured cache directory, depth scales with the payload.
Crafted request paths with backslash-based ..\ sequences caused new .html files to be written progressively further outside the cache folder ,one level above, then two , confirming a controllable, repeatable escape rather than a one-off fluke:
<project-root>\isr-cache\<name>.html <- shallow payload: stays inside (control case)
<project-root>\<name>.html <- deeper payload: now outside isr-cache
<project-root>\..\<name>.html <- deeper still: one level further up
2. Arbitrary file creation ("project folder pollution"). Any reachable location the Node process can write to can receive a new .html file at an attacker-chosen path, simply by varying the request.
3. Overwrite of existing .html files. When a request's target matches an existing file's exact base name, its content is silently replaced ,confirmed by editing a target file, sending the crafted request, and observing the content overwritten. Not limited to files inside the project.
4. Live, visitor-facing impact (highest severity). Targeting the actual served index.html (production build output, not the source file) and then requesting the homepage normally returned the corrupted content instead of the real application, confirming this reaches real visitors, not just the filesystem.
Environment
OS: Windows 11
Node: v22.11.0
@rx-angular/isr: 21.0.2
@angular/core: ^21.2.0
@angular/ssr: ^21.2.22
express: ^5.1.0 (note: library's peerDependencies declares ^4.15.2 — tested
against 5.1.0 since that's what the current Angular CLI
SSR scaffold installs by default; worth the maintainers
double-checking behavior on 4.x too)
Related to Other Issues
None known.
Methods to Resolve This
- Replace the character-denylist sanitization in
convertCacheKeyToFileName (and equivalent logic anywhere cache keys become filesystem paths) with a resolved-path containment check:
const resolved = path.resolve(cacheFolderPath, fileName);
if (!resolved.startsWith(path.resolve(cacheFolderPath) + path.sep)) {
throw new Error('Invalid cache key: resolved path escapes cache directory');
}
- Consider adding a regression test that specifically covers Windows-style backslash traversal in cache keys, since the current sanitizer's test coverage appears to only cover forward-slash cases.
Notes
I'd rather share the literal request string and a full write-up directly with a maintainer than paste it into a public issue , this repo doesn't currently have GitHub's private vulnerability reporting enabled, so I don't have a private channel to use yet.
Would any of the maintainers be open to either:
Enabling private vulnerability reporting for this repo or
Sharing a direct contact (email or otherwise) where I can send the full details?
Happy to answer any questions or provide further detail as needed. Thanks for maintaining @rx-angular/isr.
Description
This is a security vulnerability report. As per the repo's SECURITY.md, I'm filing it through the issue tracker. The root cause below is detailed enough that a working proof of concept could be reconstructed from it fairly quickly, I'm not claiming otherwise. What I am doing, as standard disclosure practice, is not posting the literal request string I used, so this issue isn't a copy-paste exploit for anyone browsing public issues. See "Notes" for what I'm proposing instead.
Expected behavior:
FileSystemCacheHandlershould only ever write cache files inside the configured cacheFolderPath`, regardless of what URL was requested.Actual behavior: on Windows, a specially crafted request path allows
FileSystemCacheHandlerto write (and overwrite).htmlfiles outside the configured cache directory, including files that are actually served to end users. I've reproduced this in a minimal test app: a crafted request causes the application's live, served entry file to be overwritten, so subsequent normal visitors receive the corrupted content instead of the real application.Root cause
convertCacheKeyToFileName()infilesystem-cache-handler.tsonly sanitizes/and?in the cache key before it's used to build a filesystem path:It does not sanitize
\. The resulting string is then passed topath.join(cacheFolderPath, '/', fileName). On Windows,path.jointreats\as a path separator and will resolve..\traversal sequences, allowingthe final write location to escape
cacheFolderPathentirely. The cache key itself is derived from the raw request URL by default, with no additional sanitization.Steps to Reproduce the Issue
@rx-angular/isrwithFileSystemCacheHandlerconfigured, per the library's documented usage in the README.revalidateset in its routedata(required for ISR to cache anything)...\) targeting a file outside the cache directory..htmlfile is created/overwritten at the traversed location rather than insidecacheFolderPath.Impact
Verified end-to-end against a minimal Angular SSR app configured per the library's documented usage (
FileSystemCacheHandler,revalidateset on the route), on Windows:1. Escapes the configured cache directory, depth scales with the payload.
Crafted request paths with backslash-based
..\sequences caused new.htmlfiles to be written progressively further outside the cache folder ,one level above, then two , confirming a controllable, repeatable escape rather than a one-off fluke:2. Arbitrary file creation ("project folder pollution"). Any reachable location the Node process can write to can receive a new
.htmlfile at an attacker-chosen path, simply by varying the request.3. Overwrite of existing
.htmlfiles. When a request's target matches an existing file's exact base name, its content is silently replaced ,confirmed by editing a target file, sending the crafted request, and observing the content overwritten. Not limited to files inside the project.4. Live, visitor-facing impact (highest severity). Targeting the actual served
index.html(production build output, not the source file) and then requesting the homepage normally returned the corrupted content instead of the real application, confirming this reaches real visitors, not just the filesystem.Environment
Related to Other Issues
None known.
Methods to Resolve This
convertCacheKeyToFileName(and equivalent logic anywhere cache keys become filesystem paths) with a resolved-path containment check:Notes
I'd rather share the literal request string and a full write-up directly with a maintainer than paste it into a public issue , this repo doesn't currently have GitHub's private vulnerability reporting enabled, so I don't have a private channel to use yet.
Would any of the maintainers be open to either:
Enabling private vulnerability reporting for this repo or
Sharing a direct contact (email or otherwise) where I can send the full details?
Happy to answer any questions or provide further detail as needed. Thanks for maintaining @rx-angular/isr.