fix: block SSRF targets in remote-server normalizeUrl#1574
fix: block SSRF targets in remote-server normalizeUrl#1574jfeldstein wants to merge 1 commit intodifferent-ai:devfrom
Conversation
The /system/servers/connect handler issues server-side fetches against a caller-supplied baseUrl, but normalizeUrl previously accepted any parseable URL. Reject loopback, link-local (169.254.0.0/16, including the EC2/GCE metadata IP), RFC 1918 private ranges, CGNAT, multicast, and non-http(s) protocols. Covers IPv6 equivalents and IPv4-mapped IPv6 forms.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@jfeldstein is attempting to deploy a commit to the Different AI Team on Vercel. A member of the Team first needs to authorize it. |
|
The following comment was made by an LLM, it may be inaccurate: |
src-opn
left a comment
There was a problem hiding this comment.
Code verification issue found.
pnpm --filter openwork-server-v2 typecheck fails because apps/server-v2/scripts/url-safety.test.ts imports the service with a .ts extension:
apps/server-v2/scripts/url-safety.test.ts:8
Error:
scripts/url-safety.test.ts(8,55): error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.
Please move this into the normal test tree or adjust the import/config so the package typecheck passes. I verified the standalone/Bun test itself passes with pnpm --filter openwork-server-v2 test ./scripts/url-safety.test.ts, but the PR should not merge while the package typecheck is broken.
Summary
TDD
normalizeUrlto reject loopback addressesWhy
The /system/servers/connect handler issues server-side fetches against a caller-supplied baseUrl, but normalizeUrl previously accepted any parseable URL. Exploit script
exploit-scripts/exploit-ssrf-normalizeurl.mjsconfirmed an attacker holding the HOST token could coerce server-v2 into making outbound HTTP requests to127.0.0.1:<any-port>(full SSRF — internal services, cloud metadata, RFC 1918 hosts).Issue
The
normalizeUrl()helper (and theconnect()flow that calls it) does not validate that the supplied baseUrl points to a public destination. A host caller can register a "remote OpenWork server" pointed at any private IP / loopback / metadata address; the server will then perform an outbound HTTPGET <baseUrl>/workspacesfrom inside the trust boundary.Scope
::1,fe80::/10,fc00::/7,::ffff:10.0.0.1).Out of scope
apps/server) — does not expose this connect path.Testing
Ran
pnpm --filter openwork-server-v2 test src/<normalizeUrl spec>pnpm --filter openwork-server-v2 typecheckBASE_URL=http://127.0.0.1:3100 HOST_TOKEN=host-secret node exploit-ssrf-normalizeurl.mjsexploit-ssrf-normalizeurl.mjs:
Result
CI status
Manual verification
cd apps/server-v2 && OPENWORK_SERVER_V2_CLIENT_TOKEN=client-secret OPENWORK_SERVER_V2_HOST_TOKEN=host-secret pnpm dev.HOST_TOKEN=host-secret. Expect the connect call to be rejected before any outbound socket opens; the script's local trap should record zero connections.POST /system/servers/connectwith a real https remote returns 200/normal error path, not the new rejection.Evidence
Risk
OPENWORK_ALLOW_PRIVATE_TARGETS=1escape hatch if a user requests it.Rollback