plugins

BareProxy CORS Plugin: Cross-Origin Rules and Preflight Answers for APIs

Status: built, number 2 of 21 in BareProxy’s build order, and out in BareProxy 0.4.0 as cors.wasm on the releases page. It only touches headers, which is why it came second. Its manual is the README in the code repository.

A web page on one origin that calls an API on another runs into the browser’s same-origin policy. The API has to say which origins may call it, with which methods and headers, in Access-Control-* response headers. Before most calls, the browser first sends an OPTIONS preflight request and waits for the answer.

Getting CORS right is fiddly, and getting it wrong is either broken (the front end can’t reach its API) or unsafe (any site can make a logged-in user’s browser call it). Every framework does it a little differently. The proxy can do it once, the same way for every API behind it.

What It Does

  • An allow list of origins, for the whole site and per path prefix, exact names only. * is allowed only where cookies aren’t, and the plugin refuses a config that combines * with credentials, which browsers reject anyway. It also refuses null, which sandboxed frames and local files all send, and wildcards like https://*.example.com.
  • Answers preflights itself. OPTIONS preflights are answered at the proxy with the allowed methods, headers and a cache time, so they never reach the app. One asking for a method or header that isn’t allowed gets 403, and the record says which.
  • Adds the headers to real responses, static files included, and Vary: Origin whenever the answer depends on the origin. Caches need it, and it’s easy to forget.
  • One list, one place. CORS headers the app sets itself are dropped, so the plugin’s list is the only one in force.
  • Refuses clearly. A call from an origin not on the list gets no CORS headers, and the request’s record says which origin it was.

Here is a config:

plugin cors /etc/bareproxy/plugins/cors.wasm
  config /etc/bareproxy/plugins/cors.conf

site api.example.com
  use cors
  route /* -> api

And the plugin’s own:

# cors.conf
origins https://app.example.com https://admin.example.com
methods GET POST PUT DELETE
headers content-type authorization
credentials on
max-age 1h

path /public/
  origins *
  credentials off

A path section takes what it doesn’t set from the top, and the longest prefix that matches wins.

What why Shows

The plugin costs one callback on the way in and one on the way out. A refused call reads in why like this: “CORS: origin https://evil.example isn’t on the list for /”. That beats reading the browser console. And a config mistake, such as * with credentials, stops at check with the line and the reason, before it goes live.

The whole program is on the plugins page.