← All posts

CORS Proxies in Browser-Based AI Systems

Browser-facing AI apps often add a CORS proxy to make front-end calls work. The real issue is not CORS itself, but where trust sits, which credentials reach the browser, and how tightly your proxy enforces origin, route, and upstream policy.

Simon Willison recently pointed to a small but useful pattern in “CORS Chat”: a browser-facing proxy which adds CORS headers so front-end code can call models and APIs directly. The detail matters because many AI teams now ship browser-native tools. They connect the UI to LLM APIs, vector search, speech services, or internal gateways from JavaScript running on the user’s device.

This looks simple. It often fails in production. CORS is not an access-control system. It is a browser policy layer. If you treat it like authentication, you expose keys, leak data across origins, or build a proxy that any site on the internet can drive. If you build AI features in the browser, you need a tighter model.

The useful takeaway from CORS Chat is not the specific service. It is the architecture question behind it. When your AI app needs browser access to an API, where do trust boundaries sit, which headers do you emit, and which credentials never cross into the client at all.

Know what CORS does, and what it does not

CORS tells the browser whether front-end code from one origin gets to read a response from another origin. It does not prove who the caller is. It does not stop a server from receiving a request. It does not protect a secret placed in client-side code.

This distinction gets missed in AI products because teams move fast from prototype to demo:

  • A React app calls an LLM endpoint.
  • The provider does not allow the browser origin.
  • A small proxy is added to inject Access-Control-Allow-Origin.
  • The proxy ends up forwarding an API key.
  • The browser app now depends on a trust model it does not enforce.

If your browser code holds a long-lived provider key, the problem is already upstream of CORS. Any user with devtools sees it. Any script running in the page context sees it. Any extension with enough privileges sees it.

For practitioners, the first check is simple. List every credential involved in an AI request path. Then sort them into two groups:

  • Safe for the browser, usually short-lived, scoped, and user-bound.
  • Server-only, usually provider secrets, signing keys, admin tokens, and broad-scope service credentials.

If a key in the second group reaches the client, CORS headers do not fix the design.

Inspect your proxy as a security boundary

A CORS proxy is often treated as a transport helper. In practice it becomes a security boundary. Once it forwards requests from browsers to upstream AI services, it decides which origins, methods, headers, and destinations are allowed.

This is where many deployments go wrong.

A weak proxy often has these traits:

  • Reflects any Origin header.
  • Allows GET, POST, PUT, DELETE, OPTIONS without need.
  • Forwards arbitrary headers from the browser.
  • Accepts any upstream URL as a query parameter.
  • Adds provider credentials to every forwarded request.
  • Returns upstream errors and headers unchanged.

That combination turns a convenience layer into an open relay. Other sites get to drive your infrastructure from their users’ browsers. Internal metadata endpoints and private admin APIs become reachable if destination filtering is weak. Response headers leak internal details. Rate limits and billing land on your account.

A tighter proxy design looks different:

  • Pin allowed origins to a short list.
  • Pin allowed upstream hosts to a short list.
  • Pin methods and content types per route.
  • Strip all client-supplied auth headers by default.
  • Inject upstream credentials server-side, per route.
  • Normalize error responses.
  • Enforce request size, timeout, and rate limits.
  • Log denied origins, denied hosts, and unusual header sets.

For AI systems, route-level policy matters. Your /chat path and your /embed path rarely need the same limits. Speech upload paths need different size caps from text completion paths. Admin evaluation routes should never be browser-callable unless they use separate user-scoped auth.

Preflight behavior exposes design bugs early

Browsers send a preflight OPTIONS request before many cross-origin calls. Teams often treat preflight failures as an annoying config issue. They are a useful signal.

If a request needs many custom headers and methods, stop and inspect why. Browser-facing AI calls often drift into this pattern:

  • Authorization: Bearer <provider-key>
  • Several custom tracing headers
  • Session identifiers copied from another system
  • Non-standard content types
  • Broad Access-Control-Allow-Headers in response

This is friction for a reason. The browser is telling you the request shape is sensitive.

A practical verification routine:

  1. Open network tools and capture the preflight and main request.
  2. Compare requested headers with the minimal set the route needs.
  3. Remove every header not required for the upstream service.
  4. Check whether the server replies with Vary: Origin when origin-specific responses are served.
  5. Check whether credentials mode is in use, and whether Access-Control-Allow-Credentials: true is present only when needed.
  6. Verify Access-Control-Allow-Origin is never * on credentialed flows.

The Vary: Origin point is easy to miss. Without it, shared caches risk serving a response prepared for one origin to another. In AI apps with per-tenant browser origins, this becomes a messy source of cross-tenant bugs.

Preflight caching also matters. A large Access-Control-Max-Age reduces latency, but it slows the effect of policy changes during incident response. If you need to revoke an origin or header quickly, long cache lifetimes work against you.

Browser-native AI needs short-lived credentials

There are valid cases for direct browser calls. Real-time transcription, low-latency chat streaming, and user-side uploads often benefit from them. The pattern works when the browser uses narrow, temporary credentials issued by your backend.

The model is familiar:

  • Your backend authenticates the user.
  • Your backend creates a short-lived token or signed request with limited scope.
  • The browser uses it for one model, one route, one tenant, or one upload window.
  • Expiry is measured in minutes, not days.

This reduces blast radius. It also gives you cleaner revocation and audit trails.

Where teams slip:

  • Tokens are long-lived to reduce refresh logic.
  • Scope is broad because one token is reused across features.
  • Tenant identity lives in a client-controlled header.
  • The upstream API key is still embedded as a fallback path.

For multi-tenant AI software, bind temporary credentials to server-validated claims. Do not let the browser assert tenant, plan, or model access on its own. If your proxy receives X-Tenant-ID from the client and trusts it, you have not built isolation. You have moved it into a header.

This also affects cost control. AI APIs are metered infrastructure. A browser path without strict scoping and quotas becomes an abuse path, whether the traffic comes from your own users, embedded third-party pages, or automated browsers.

Streaming, uploads, and error paths need separate review

AI applications often use SSE, WebSockets, chunked uploads, and long-running responses. Teams test the happy path and miss what happens around it.

Review these areas separately.

Streaming responses

  • Confirm CORS policy for the exact transport in use.
  • Check proxy buffering behavior.
  • Make sure disconnects close upstream model sessions.
  • Cap concurrent streams per user and per origin.

File and audio uploads

  • Restrict content types by route.
  • Enforce file size before forwarding upstream.
  • Scan filename handling and storage keys.
  • Check whether pre-signed upload URLs expose broad bucket access.

Error handling

  • Strip upstream stack traces and internal IDs.
  • Normalize 4xx and 5xx bodies.
  • Log enough detail server-side for debugging.
  • Avoid reflecting arbitrary upstream header values.

These paths matter because AI providers often return rich metadata. Request IDs, model identifiers, usage data, and backend errors are useful for operators. They are not all useful in the browser. Decide what the client needs, then emit only that.

If your front end fetches from a custom domain, basic web checks help too. TLS setup, redirect behavior, and security headers affect the reliability of browser API traffic. Pigfox’s Website Legit Check is built for website trust signals, and it fits this narrow operational review when you want one pass over headers, redirects, and TLS.

What to watch next

Browser-native AI is growing because it cuts latency and simplifies deployment. It also pushes more trust decisions to the edge. Expect more teams to adopt ephemeral tokens, route-scoped proxies, and stricter origin policies as usage grows.

The practical next step is to treat every CORS decision as part of your authentication and tenancy design review. Inventory which secrets touch the browser. Pin origins and upstream destinations. Keep tokens short-lived. Test preflight and error paths with the same care as the main prompt flow. That is where many AI integrations break first.