Skip to content

[BUG] exposeHeaders: true echoes request headers into CORS response headers on non-preflight requests #76

Description

@Alice39s

Summary

I ran into a subtle issue with the default allowedHeaders: true and exposeHeaders: true behavior on non-preflight requests, and wanted to share a concrete real-world example where it causes problems.

What happens

When either option is set to true, the plugin calls processHeaders(request.headers) on every non-preflight response. This extracts all incoming request header keys and echoes them back as the value of the corresponding CORS response header.

Real-world impact

Our API sits behind Cloudflare, so every request arrives with a long tail of proxy headers: cf-connecting-ip, cf-ipcity, cf-ipcontinent, cf-ray, cdn-loop, x-forwarded-for, x-forwarded-proto, all the sec-fetch-* and sec-ch-ua-* headers, and more. With the current defaults, our responses ended up looking like this:

access-control-allow-headers:
  host, user-agent, content-length, accept,
  accept-encoding, accept-language, cache-control, connection, dnt, origin,
  pragma, referer, sec-fetch-dest, sec-fetch-mode, cdn-loop, cf-connecting-ip,
  cf-ipcity, cf-ipcontinent, cf-ipcountry, cf-iplatitude, cf-iplongitude,
  cf-postal-code, cf-ray, cf-timezone, cf-visitor, cf-warp-tag-id, priority,
  sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, sec-fetch-site,
  x-forwarded-for, x-forwarded-proto

access-control-expose-headers:
  host, user-agent, content-length, accept,
  accept-encoding, accept-language, cache-control, connection, dnt, origin,
  pragma, referer, sec-fetch-dest, sec-fetch-mode, cdn-loop, cf-connecting-ip,
  cf-ipcity, cf-ipcontinent, cf-ipcountry, cf-iplatitude, cf-iplongitude,
  cf-postal-code, cf-ray, cf-timezone, cf-visitor, cf-warp-tag-id, priority,
  sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, sec-fetch-site,
  x-forwarded-for, x-forwarded-proto

The exposeHeaders list is particularly problematic: Access-Control-Expose-Headers is meant to declare which response headers the browser may expose to JavaScript, but here it's being populated with request header names — headers the server never sends back as response headers.

Workaround

We worked around it by explicitly setting both options:

cors({
  allowedHeaders: ["content-type"],
  exposeHeaders: [],
})

This works, but the default still feels unexpected — especially for exposeHeaders, where echoing request headers is semantically an odd default.

Existing PR

I noticed there's already a PR open that addresses part of this: #66 (Allow allowedHeaders and exposeHeaders to receive false).

It's been open for about a year without activity. Adding false as an accepted value would be a clean way to opt out entirely without needing to pass an empty array as a workaround.

Suggestion

Would the team be open to one of these?

  1. Review and merge Allow allowedHeaders and exposeHeaders to receive false #66 — it's a small, backwards-compatible types change that gives users explicit control
  2. Reconsider the default for exposeHeaders on non-preflight requests — echoing all request headers doesn't quite match the purpose of the header

Thanks for taking a look, and happy to help with a PR if there's a preferred direction!

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions