Skip to content

fix(browser-runtime): block SSRF via SKILL_API.fetch bridge#222

Open
sebastiondev wants to merge 2 commits intoAIPexStudio:mainfrom
sebastiondev:fix/cwe918-skill-api-fetch-3649
Open

fix(browser-runtime): block SSRF via SKILL_API.fetch bridge#222
sebastiondev wants to merge 2 commits intoAIPexStudio:mainfrom
sebastiondev:fix/cwe918-skill-api-fetch-3649

Conversation

@sebastiondev
Copy link
Copy Markdown
Contributor

Summary

Block server-side request forgery (SSRF) via the SKILL_API.fetch bridge that user-installed skills use to make HTTP requests from the host extension context.

  • CWE-918: Server-Side Request Forgery
  • Severity: High
  • Affected file: packages/browser-runtime/src/lib/vm/skill-api.ts (createSkillAPIBridge().fetch)

Vulnerability

Skills run inside a QuickJS VM sandbox with a virtualized filesystem (ZenFS). The VM itself has no network primitives — the only way a skill can reach the network is the SKILL_API.fetch bridge, which forwards the request to the host fetch running in the extension service worker.

Before this change, the bridge passed the URL to fetch() unchanged. A malicious or compromised skill could therefore reach hosts that should never be reachable from skill code:

  • Cloud instance metadata: http://169.254.169.254/latest/meta-data/...
  • LAN devices and admin panels: http://192.168.1.1/, http://10.0.0.1/
  • Loopback services: http://127.0.0.1:<port>/, http://localhost/
  • .local / .internal / .localhost suffixes
  • IPv6 loopback / link-local / ULA, including IPv4-mapped IPv6 (::ffff:127.0.0.1)
  • Non-http(s) schemes such as file://

The skill could then exfiltrate the response by calling SKILL_API.fetch again to an attacker-controlled origin.

Data flow: skill JS in QuickJS VM → SKILL_API.fetch(url, …) (attacker-controlled url) → host fetch() in the service worker → arbitrary internal endpoint.

Fix

Add a single chokepoint in the bridge that validates URLs before they reach the host fetch:

  1. New url-guard.ts exports assertSkillFetchUrlAllowed(url) which:
    • Requires http: or https: scheme.
    • Rejects a curated BLOCKED_HOSTNAMES set (localhost, ip6-localhost, etc.) and the .local / .internal / .localhost suffixes.
    • Rejects the full RFC1918 IPv4 set plus 127.0.0.0/8, 0.0.0.0/8, 169.254.0.0/16, 224.0.0.0/4, 240.0.0.0/4.
    • Rejects IPv6 ::1, ::, fe80::/10, fc00::/7, ff00::/8, and IPv4-mapped IPv6 in both dotted (::ffff:127.0.0.1) and hex (::ffff:7f00:1) forms.
  2. skill-api.ts calls the guard, then sets redirect: "error" on the request so a public host cannot 3xx-redirect into the private range.

The guard is placed at the VM↔host boundary — the only path skill code has to the network — so it cannot be bypassed within the existing architecture.

Tests

Added packages/browser-runtime/src/lib/vm/skill-api.test.ts with coverage for both layers:

assertSkillFetchUrlAllowed:

  • allows public https URLs
  • blocks non-http(s) schemes (file:, ftp:, chrome:, …)
  • blocks loopback hostnames and addresses
  • blocks 169.254.169.254 cloud metadata
  • blocks RFC1918 ranges (10/8, 172.16/12, 192.168/16)
  • blocks IPv4-mapped IPv6 loopback
  • blocks IPv6 link-local and ULA
  • blocks .local / .internal / .localhost suffixes
  • permits real public IPs (e.g. 1.1.1.1, 8.8.8.8)

SKILL_API.fetch SSRF guard (with mocked host fetch):

  • forwards public URLs to host fetch
  • rejects cloud metadata without invoking host fetch
  • rejects localhost without invoking host fetch
  • rejects file:// scheme attempts
  • forces redirect: "error" (defense-in-depth)

Repo npm run preflight passes (lint + typecheck + tests, including noUncheckedIndexedAccess).

Why this is exploitable

The precondition is "user installs a malicious skill". That precondition alone does not grant network access to private ranges: the QuickJS VM has no networking, ZenFS is fully virtual, and SKILL_API.downloadFile defaults to a Save-As dialog. The fetch bridge is the unique path from skill code to the network, and before this fix it had no host/scheme allow-list. The fix therefore provides a real privilege reduction beyond what the precondition already grants.

The known residual risk is DNS rebinding (a hostname that resolves to a public IP at validation time and to 127.0.0.1 at connect time). That would require an additional DNS-rebinding control in the host environment to fully mitigate and is out of scope for this change.

Adversarial review

Before submitting, we tried to disprove this. The QuickJS sandbox does block code execution and direct socket access, but it explicitly does not mediate the host-side fetch bridge — that's the only network primitive exposed to skills, and prior to this patch it was unfiltered. We checked whether browser-level protections (CORS, mixed-content) would help: they don't, because the fetch runs in the extension service worker with extension privileges and bypasses CORS for permitted origins. The redirect-based bypass is closed by redirect: "error". The fix is at the right boundary and is the minimum change needed.

cc @lewiswigmore

Skills run as untrusted user-supplied code in a QuickJS VM but their
fetch() bridge previously forwarded any URL to host fetch() in the
extension service worker. A malicious skill could request internal
addresses (cloud metadata 169.254.169.254, localhost services,
RFC1918 ranges) and read the response.

Add a URL guard that:
- restricts schemes to http/https
- rejects loopback, link-local, RFC1918, ULA, multicast, and
  reserved IPv4/IPv6 ranges (incl. ::ffff: IPv4-mapped forms)
- rejects 'localhost' and .local/.internal/.localhost suffixes
- forces redirect: 'error' so a public 3xx cannot escape the guard

Includes unit tests covering the guard and the bridge integration.
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