From 7cbc9c25edf27592bae4af83b3e6af878704fad5 Mon Sep 17 00:00:00 2001 From: David Crowe Date: Thu, 23 Jul 2026 14:15:22 -0700 Subject: [PATCH] Correct proxyabl SSRF marketing to match what actually runs (#43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit proxyabl-core's assertUrlSafe/executeProxyRequest (DNS-resolving private-IP blocking, host allowlist, redirect:"manual", size/timeout caps) are exported and unit-tested but have zero consumers outside their own tests. The README and docs implied the gateway proxy runs this engine; it does not. Decision: correct the marketing (the issue's "Or" option), not wire the engine into the router — because the router's model is genuinely different, not a bug to paper over: - The three user-facing router fetches (mcpHandler, restToolHandler, proxyHandler) all forward to an operator-CONFIGURED backend (functionsBase / proxy target). The caller controls the tool name / path, not the host, which is pinned to the configured backend via a host+protocol equality check. - executeProxyRequest is built for the opposite case — proxying arbitrary, caller-influenced URLs against an allowlist. Dropping it into the router would (a) reject configured backends that are legitimately internal (private-IP blocking), (b) change response semantics (it buffers+caps and throws on non-2xx instead of passing upstream status/body through), and (c) block upstream redirects the router may rely on. That is a breaking change to live gateway behavior to satisfy a marketing line — backwards. - OSS has no arbitrary-URL / custom-connector path today (executeProxyRequest's ancestor served custom HTTP tools that live only in prod), so the engine has no natural OSS consumer yet. Changes (docs only): - README module table: proxyabl row now describes the router accurately (configured-backend, path-allowlisted, identity-aware) and points to the proxyabl-core SSRF primitive. - README: added an "SSRF note" spelling out the two threat models. - docs/packages.md: split the proxyabl entry into the core SSRF-safe primitive vs the pass-through Express router, and noted the router does not route through the engine + the convergence direction. - proxyabl-core/README: added a scope note so a reader (or a reviewer grepping assertUrlSafe) sees the primitive is intentional and standalone, not dead. The proxyabl-core README's own SSRF description was already accurate for the primitive and is unchanged except for the note. The real fix — one canonical proxyabl-core engine consumed by both the OSS router and prod's fork — is the convergence epic; flagged, not attempted here. --- README.md | 4 +++- docs/packages.md | 8 +++++++- packages/proxyabl-core/README.md | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d4b42b4..345de5d 100644 --- a/README.md +++ b/README.md @@ -88,9 +88,11 @@ Each layer ships as a framework-agnostic `-core` package plus an Express middlew | `@gatewaystack/transformabl` | [![npm](https://img.shields.io/npm/v/@gatewaystack/transformabl)](https://www.npmjs.com/package/@gatewaystack/transformabl) | PII detection, redaction, safety classification | | `@gatewaystack/validatabl` | [![npm](https://img.shields.io/npm/v/@gatewaystack/validatabl)](https://www.npmjs.com/package/@gatewaystack/validatabl) | Deny-by-default policy engine, scope/permission enforcement | | `@gatewaystack/limitabl` | [![npm](https://img.shields.io/npm/v/@gatewaystack/limitabl)](https://www.npmjs.com/package/@gatewaystack/limitabl) | Rate limits, budget tracking, agent guard | -| `@gatewaystack/proxyabl` | [![npm](https://img.shields.io/npm/v/@gatewaystack/proxyabl)](https://www.npmjs.com/package/@gatewaystack/proxyabl) | Auth mode routing, SSRF protection, identity-aware proxy | +| `@gatewaystack/proxyabl` | [![npm](https://img.shields.io/npm/v/@gatewaystack/proxyabl)](https://www.npmjs.com/package/@gatewaystack/proxyabl) | Auth-mode routing, identity-aware proxy to a configured backend (path-allowlisted). SSRF-safe fetch for arbitrary URLs is a `proxyabl-core` primitive — see note below. | | `@gatewaystack/explicabl` | [![npm](https://img.shields.io/npm/v/@gatewaystack/explicabl)](https://www.npmjs.com/package/@gatewaystack/explicabl) | Structured audit logging, health endpoints | +> **SSRF note (proxyabl).** The `proxyabl` Express router forwards to an operator-**configured** backend, with tool-name sanitization, path allowlisting, and host/protocol pinning to that backend — the target host is not caller-controlled. The DNS-resolving, private-IP-blocking SSRF engine (`assertUrlSafe` / `executeProxyRequest`) lives in `@gatewaystack/proxyabl-core` and is the primitive to use when you proxy **arbitrary, caller-influenced URLs**; the bundled router does not currently route through it (its threat model is different — a configured backend may legitimately be internal). See [`docs/packages.md`](docs/packages.md#proxyabl). + ## Full stack example Wire all six layers together. Each is optional — use only what you need. diff --git a/docs/packages.md b/docs/packages.md index c636227..3621f5e 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -30,7 +30,13 @@ GatewayStack ships as composable npm packages. Each governance layer has a `-cor `@gatewaystack/proxyabl-core` / `@gatewaystack/proxyabl` -**Execution Control & Identity-Aware Routing.** Five auth modes (API key, forward bearer, service OAuth, user OAuth, none), SSRF protection (host allowlist, private IP blocking), HTTP proxy with timeout/redirect/size controls, multi-provider registry. Express middleware serves PRM/OIDC metadata, enforces scope-to-tool mappings, and injects verified identity into downstream headers. +**Execution Control & Identity-Aware Routing.** Five auth modes (API key, forward bearer, service OAuth, user OAuth, none) and a multi-provider registry. + +**`proxyabl-core` — SSRF-safe fetch primitive.** `assertUrlSafe` / `executeProxyRequest` provide host-allowlisting, DNS-resolving private-IP blocking (IPv4 + IPv6, incl. IPv4-mapped IPv6), protocol enforcement, redirect blocking (`redirect: "manual"`), and timeout/response-size caps. Use these directly when you proxy **arbitrary, caller-influenced URLs** — that is the case SSRF protection is for. + +**`proxyabl` — Express gateway router.** Forwards to an operator-**configured** backend (`functionsBase` / proxy target). The caller controls the tool name / path, not the host: the router sanitizes the tool name, enforces a path allowlist, and pins the resolved URL's host + protocol to the configured backend. It is a pass-through proxy and **does not currently route through `proxyabl-core`'s `assertUrlSafe` engine** — the two have different threat models (a configured backend may legitimately be an internal address, which the private-IP-blocking engine would reject). The Express middleware also serves PRM/OIDC metadata, enforces scope-to-tool mappings, and injects verified identity into downstream headers. + +> Converging these — one canonical `proxyabl-core` engine consumed by both the OSS router and the production gateway (which forked an equivalent) — is tracked in the proxyabl convergence work. Until then, treat `assertUrlSafe` / `executeProxyRequest` as an available primitive, not something the bundled router runs for you. ## explicabl diff --git a/packages/proxyabl-core/README.md b/packages/proxyabl-core/README.md index d3e567f..d078b43 100644 --- a/packages/proxyabl-core/README.md +++ b/packages/proxyabl-core/README.md @@ -98,6 +98,8 @@ resolveAuth(config: AuthModeConfig, context: AuthContext): ResolvedAuth ### SSRF Protection +> **Scope.** `assertUrlSafe` / `executeProxyRequest` are the primitive for proxying **arbitrary, caller-influenced URLs** — call them directly from your handler. The Express router in [`@gatewaystack/proxyabl`](https://www.npmjs.com/package/@gatewaystack/proxyabl) is a different thing: it forwards to an operator-**configured** backend and does not route through this engine (a configured backend may legitimately be internal, which private-IP blocking would reject). Importing the router does not give you this SSRF check automatically. + ```ts assertUrlSafe(opts: UrlSafetyOptions): Promise ```