Skip to content

fix(viewer): prevent path-traversal SSRF in proxy (CWE-918)#898

Open
sebastiondev wants to merge 1 commit into
rohitg00:mainfrom
sebastiondev:fix/cwe918-server-viewer-11d9
Open

fix(viewer): prevent path-traversal SSRF in proxy (CWE-918)#898
sebastiondev wants to merge 1 commit into
rohitg00:mainfrom
sebastiondev:fix/cwe918-server-viewer-11d9

Conversation

@sebastiondev

@sebastiondev sebastiondev commented Jun 10, 2026

Copy link
Copy Markdown

Summary

The viewer proxy in src/viewer/server.ts has a path-traversal vulnerability (CWE-918) in the proxyToRestApi() function. An attacker with local HTTP access to the viewer port can escape the intended /agentmemory/ path prefix and reach arbitrary upstream routes while the proxy auto-attaches the AGENTMEMORY_SECRET Bearer token.

This PR adds path normalization before the prefix check, closing the traversal gap.

Vulnerability Details

CWE: CWE-918 (Server-Side Request Forgery)
Affected file: src/viewer/server.ts, function proxyToRestApi() (line 417)
Severity: Medium (exploitable under specific but realistic preconditions)

Data flow

  1. The viewer receives an HTTP request and extracts req.url at line 278.
  2. The raw pathname is split from the query string at line 280 — no normalization occurs.
  3. In proxyToRestApi(), the pathname is checked with startsWith("/agentmemory/") to decide routing.
  4. A path like /agentmemory/../../secret-route passes this prefix check (it literally starts with /agentmemory/).
  5. The path is then embedded into a URL string and passed to fetch(), which internally normalizes .. segments via its URL parser — resolving /agentmemory/../../secret-route to /secret-route.
  6. The request reaches the upstream REST API at /secret-route (outside /agentmemory/) with the privileged Bearer token attached.

The core issue is a TOCTOU mismatch: the prefix check operates on the raw path, but fetch() normalizes it before making the request.

Threat model

The vulnerability is exploitable when:

  • AGENTMEMORY_SECRET is set (REST API authentication is enabled)
  • The viewer is running in default loopback mode (no inbound Bearer required for the viewer itself)
  • An attacker has local HTTP access to the viewer port (3113) but does not know the secret
  • The upstream iii engine exposes routes outside /agentmemory/

In this configuration, the viewer intentionally acts as an unauthenticated bridge to the authenticated /agentmemory/* API surface. The path traversal breaks that constraint by allowing requests to non-/agentmemory/ routes with the auto-attached Bearer token.

Before submitting, we verified that this isn't blocked by other defenses: the host-header allowlist and CORS restrictions prevent cross-origin attacks but don't prevent a local process from sending raw HTTP requests. The loopback bind prevents remote exploitation but doesn't protect against other processes on the same machine. In non-loopback (Fly) deployments, inbound Bearer is required, which makes the traversal redundant since the attacker would already know the secret.

Proof of Concept

# Precondition: agentmemory running with AGENTMEMORY_SECRET set,
# viewer on default loopback mode (port 3113).
#
# curl normalizes ".." by default, so --path-as-is is needed to
# send the raw traversal path to Node's HTTP server:

curl --path-as-is http://127.0.0.1:3113/agentmemory/../../some-internal-route

# Without the fix: the viewer passes the startsWith("/agentmemory/")
# check, then fetch() resolves the path to /some-internal-route and
# forwards the request WITH the Bearer token.
#
# With the fix: the viewer normalizes the path first, sees it resolves
# outside /agentmemory/, and returns HTTP 400.

Node.js's built-in HTTP server passes raw .. segments through in req.url without normalization — this was confirmed experimentally and is documented Node behavior.

Fix Description

The fix adds a normalizeProxyPath() function that:

  1. Parses the raw path using new URL(raw, "http://localhost") — this applies the same URL normalization that fetch() uses internally, resolving all .. segments, percent-encoded variants (%2e%2e, %2E%2E), and backslash tricks.
  2. Checks that the normalized pathname still starts with /agentmemory/.
  3. Returns the normalized path for use in the upstream URL, or null if the path escapes the prefix.

This eliminates the TOCTOU gap: the prefix check and the actual upstream request now operate on the same normalized path. Invalid paths are rejected with HTTP 400 before reaching fetch().

Testing

A new test file test/viewer-proxy-path.test.ts covers 7 scenarios:

Test Input Expected
Literal traversal /agentmemory/../../admin 400, no upstream hit
Percent-encoded %2e%2e /agentmemory/%2e%2e/%2e%2e/admin 400, no upstream hit
Mixed-case %2E%2E /agentmemory/%2E%2E/%2E%2E/admin 400, no upstream hit
Single traversal /agentmemory/../other 400, no upstream hit
Deep traversal /agentmemory/foo/../../bar 400, no upstream hit
Normal path (livez) /agentmemory/livez 200, forwarded correctly
Normal path with query /agentmemory/memories?latest=true 200, forwarded correctly

Each test spins up a recording upstream server and verifies both the HTTP status and that no request reached the upstream for blocked paths.

Files Changed

  • src/viewer/server.ts — Added normalizeProxyPath() and integrated it into proxyToRestApi()
  • test/viewer-proxy-path.test.ts — New test file for path traversal defense

Submitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.

Summary by CodeRabbit

  • Bug Fixes
    • Improved security for the agent memory proxy endpoint by implementing path normalization and validation. The proxy now blocks directory traversal attempts (both raw and encoded variants) and rejects invalid paths with an HTTP 400 error response.

The viewer proxy in proxyToRestApi() forwards requests to the local REST
API, auto-attaching the AGENTMEMORY_SECRET bearer token. A path like
/agentmemory/../../admin passes the /agentmemory/ prefix check but gets
normalized by fetch() URL parser to /admin, escaping the intended prefix
while carrying the privileged bearer token.

Add normalizeProxyPath() that uses new URL() to canonicalize the path
(matching what fetch() will do internally) and then verifies the result
still starts with /agentmemory/. Requests with traversal sequences
(../, %2e%2e/, %2E%2E/) are rejected with 400 before reaching fetch().

Includes test suite covering literal, percent-encoded, and mixed-case
traversal payloads, plus pass-through verification for legitimate paths.

Signed-off-by: Sebastion <sebastion@sebastion.dev>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

@sebastiondev is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 104f0a41-95dd-467a-a6b2-9115da92951b

📥 Commits

Reviewing files that changed from the base of the PR and between f3dc7f8 and 8f7f643.

📒 Files selected for processing (2)
  • src/viewer/server.ts
  • test/viewer-proxy-path.test.ts

📝 Walkthrough

Walkthrough

Adds path normalization validation to prevent directory-traversal attacks (../ and percent-encoded variants) on /agentmemory/* proxy requests. The normalizeProxyPath() function validates paths remain under the prefix; proxyToRestApi rejects invalid paths with HTTP 400 before forwarding. Comprehensive tests verify rejection of traversal attempts and allowed paths.

Changes

Path-traversal defense for proxy requests

Layer / File(s) Summary
Path normalization validation function
src/viewer/server.ts
normalizeProxyPath(raw) uses Node's path.normalize() to resolve . and .. segments, then validates the result stays under /agentmemory/, returning null if normalization fails or escapes the prefix.
Proxy handler validation integration
src/viewer/server.ts
proxyToRestApi calls normalizeProxyPath() on the candidate upstream path, returns HTTP 400 with { error: "invalid proxy path" } if validation fails, and forwards only validated normalized paths to the local REST API.
Path-traversal defense test suite
test/viewer-proxy-path.test.ts
Vitest suite starting upstream and viewer servers with a low-level request helper; validates rejection of raw ../, percent-encoded %2e%2e, and mixed-case traversal variants with HTTP 400 and zero upstream requests; validates allowed paths like /agentmemory/livez and /agentmemory/memories?latest=true return HTTP 200 and reach the upstream once with expected URL.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A path once twisted, now made true,
Normalization guards what users do,
No .. shall pass the /agentmemory/ gate,
With tests so thorough, our defenses are great! 🛡️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main security fix: preventing path-traversal SSRF vulnerabilities (CWE-918) in the viewer proxy, which matches the primary objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant