Source: Source pull request number: 898 in rohitg00/agentmemory (URL omitted to avoid GitHub cross-reference)
Title: fix(viewer): prevent path-traversal SSRF in proxy (CWE-918)
Author: sebastiondev
State: open
Draft: no
Merged: no
Head: sebastiondev/agentmemory:fix/cwe918-server-viewer-11d9 @ 8f7f643
Base: main @ f3dc7f8
Labels: (none)
Changed files: 0
Commits: 0
Created: 2026-06-10T23:19:20Z
Updated: 2026-06-10T23:22:46Z
Closed: (not closed)
Merged at: (not merged)
Original PR body:
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
- The viewer receives an HTTP request and extracts
req.url at line 278.
- The raw pathname is split from the query string at line 280 — no normalization occurs.
- In
proxyToRestApi(), the pathname is checked with startsWith("/agentmemory/") to decide routing.
- A path like
/agentmemory/../../secret-route passes this prefix check (it literally starts with /agentmemory/).
- 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.
- 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 f<!-- -->ix: the viewer passes the startsWith("/agentmemory/")
# check, then fetch() res<!-- -->olves the path to /some-internal-route and
# forwards the request WITH the Bearer token.
#
# With the f<!-- -->ix: the viewer normalizes the path first, sees it res<!-- -->olves
# 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:
- 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.
- Checks that the normalized pathname still starts with
/agentmemory/.
- 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.
Local branch:
Fork PR:
Fork decision:
Verification:
Notes:
Source: Source pull request number: 898 in rohitg00/agentmemory (URL omitted to avoid GitHub cross-reference)
Title: fix(viewer): prevent path-traversal SSRF in proxy (CWE-918)
Author: sebastiondev
State: open
Draft: no
Merged: no
Head: sebastiondev/agentmemory:fix/cwe918-server-viewer-11d9 @ 8f7f643
Base: main @ f3dc7f8
Labels: (none)
Changed files: 0
Commits: 0
Created: 2026-06-10T23:19:20Z
Updated: 2026-06-10T23:22:46Z
Closed: (not closed)
Merged at: (not merged)
Original PR body:
Summary
The viewer proxy in
src/viewer/server.tshas a path-traversal vulnerability (CWE-918) in theproxyToRestApi()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 theAGENTMEMORY_SECRETBearer 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, functionproxyToRestApi()(line 417)Severity: Medium (exploitable under specific but realistic preconditions)
Data flow
req.urlat line 278.proxyToRestApi(), the pathname is checked withstartsWith("/agentmemory/")to decide routing./agentmemory/../../secret-routepasses this prefix check (it literally starts with/agentmemory/).fetch(), which internally normalizes..segments via its URL parser — resolving/agentmemory/../../secret-routeto/secret-route./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_SECRETis set (REST API authentication is enabled)/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
Node.js's built-in HTTP server passes raw
..segments through inreq.urlwithout normalization — this was confirmed experimentally and is documented Node behavior.Fix Description
The fix adds a
normalizeProxyPath()function that:new URL(raw, "http://localhost")— this applies the same URL normalization thatfetch()uses internally, resolving all..segments, percent-encoded variants (%2e%2e,%2E%2E), and backslash tricks./agentmemory/.nullif 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.tscovers 7 scenarios:/agentmemory/../../admin%2e%2e/agentmemory/%2e%2e/%2e%2e/admin%2E%2E/agentmemory/%2E%2E/%2E%2E/admin/agentmemory/../other/agentmemory/foo/../../bar/agentmemory/livez/agentmemory/memories?latest=trueEach 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— AddednormalizeProxyPath()and integrated it intoproxyToRestApi()test/viewer-proxy-path.test.ts— New test file for path traversal defenseSubmitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.
Summary by CodeRabbit
Local branch:
Fork PR:
Fork decision:
Verification:
Notes: