---
title: "CORS"
url: "/reference/filters/http/cors/"
kind: "reference"
summary: "HTTP filter that answers preflight requests and stamps CORS headers on responses."
---

> Arion site index: [/llms.txt](/llms.txt)

# CORS

HTTP filter that answers preflight requests and stamps CORS headers on responses.

| Attachment | Type URL (`@type`) | Per-route override | Build feature |
|---|---|---|---|
| HTTP filter chain (`http_filters`) | `type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors` and `type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy` | `disabled` only | None |

## What it does

The CORS filter handles cross-origin browser traffic: it answers `OPTIONS` preflight requests (those carrying `access-control-request-method`) directly, without going upstream, and adds `access-control-*` headers to responses for allowed origins. Two type URLs configure the same filter: `Cors` enables a permissive default policy, `CorsPolicy` sets an explicit one.

## Minimal configuration

The fragment is an entry in `http_filters`, before the router:

```yaml,arion=fragment
- name: cors
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy
    allow_origin_string_match:
      - exact: https://app.example.com
    allow_methods: "GET, POST, PUT, DELETE"
    allow_headers: "authorization, content-type"
    expose_headers: "x-request-id"
    allow_credentials: true
    max_age: "3600"
```

## Fields

`Cors` takes no fields and enables a permissive default policy: all origins, methods and headers allowed, all headers exposed, no credentials, `max_age` 86400. `CorsPolicy` replaces those defaults entirely with:

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `allow_origin_string_match` | list of string matchers | Yes | — | Origins allowed to make cross-origin requests (`exact`, `prefix`, `suffix`, `contains` or `safe_regex`, each with optional `ignore_case`). Left empty, no origin matches. |
| `allow_methods` | string | Yes | — | Comma-separated methods for `access-control-allow-methods`. An empty value fails to parse at startup. |
| `allow_headers` | string | No | none | Comma-separated headers allowed in preflight; `*` allows all when credentials are off. `mcp-session-id` is always allowed. |
| `expose_headers` | string | No | none | Comma-separated headers for `access-control-expose-headers`. `mcp-session-id` is always exposed. |
| `allow_credentials` | bool | No | `false` | Send `access-control-allow-credentials: true`. |
| `max_age` | string | No | unset | Preflight cache lifetime in seconds, as a string. |

`filter_enabled`, `shadow_enabled`, `allow_private_network_access` and `forward_not_matching_preflights` are unsupported and refused at startup.

## Where policies attach

In Arion the policy attaches at the filter chain: `CorsPolicy` appears directly in `http_filters` and applies to every route served by that connection manager. Envoy's other attachment points — the virtual host `cors` field and `CorsPolicy` inside `typed_per_filter_config` — are not supported and are rejected at startup. Per route you can only switch the filter off with a `FilterConfig` override that sets `disabled: true`.

## Ordering and interactions

Place CORS early in the chain — before authentication — so preflights, which browsers send without credentials, are answered instead of failing JWT checks. The filter never rejects a request itself: traffic from a disallowed origin passes through without CORS headers and the browser blocks the response. Allowed non-preflight requests gain their `access-control-*` headers on the response path.
