Sitelet https://github.com/rx-angular/rx-angular/issues/1944
Skip to content

Path Traversal in FileSystemCacheHandler leads to Arbitrary File Write/Overwrite (Windows) #1944

Description

@R1zoro

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

  1. Create an Angular SSR app using @rx-angular/isr with FileSystemCacheHandler configured, per the library's documented usage in the README.
  2. Ensure at least one route has revalidate set in its route data (required for ISR to cache anything).
  3. Deploy/run on a Windows host.
  4. 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.
  5. Observe that a .html file is created/overwritten at the traversed location rather than inside cacheFolderPath.
  6. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions