The CORS policy accepts any request header from any origin
What this check looks for
The preflight said another site may send any header it likes with its request — on a policy that does not restrict which sites may ask. That includes headers your application treats as meaningful.
Why it matters
Applications routinely trust headers: an API key header, an X-Forwarded-* value, a tenant or impersonation header, a header a proxy is assumed to be the only source of. A wildcard grant on an unrestricted origin lets another site set all of them from the visitor's browser.
When the check passes, your report says: “The request headers accepted cross-origin are a named list”.
What it costs your score
When this check fails it removes 4 points from your Web security score, before the status, confidence and repeat multipliers are applied. Web security carries a weight of 8 in the overall score.
It shares the web-security.cors family ceiling of 35 points: however many findings that family produces, together they cannot remove more than that from Web security. One underlying problem showing up in several places is still one problem.
- Severity
- low
- Default confidence
- confirmed
- Status when triggered
- warn
- Deduction
- 4 points
- Family cap
- web-security.cors · 35
- Category
- Web security
- Module
- Web cors
- Fix owned by
- user
- In the ruleset since
- 2026.09
How to fix it
Fix the origin policy, then name the specific request headers the endpoint expects.
A wildcard header grant on an origin policy that allows everyone lets any site set headers your application trusts.
Replace the wildcard or reflected origin with a fixed allow-list compared by exact equality, returning the matched origin and
Vary: Origin.Replace
*with the headers this route actually reads, for exampleAuthorization, Content-Type.Name
Authorizationexplicitly if the endpoint uses it —*does not cover it, and it is ignored entirely for credentialed requests.Audit any header the application treats as trusted, such as
X-Forwarded-Foror a tenant identifier, and make sure it is stripped at the edge rather than accepted from the client.
How to confirm it worked
curl -sSI -X OPTIONS -H 'Origin: https://app.example.com' -H 'Access-Control-Request-Method: GET' -H 'Access-Control-Request-Headers: authorization' https://‹host›/ | grep -i '^access-control-allow-headers:'
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Headers: Authorization, Content-Type
Vary: OriginA named slot like ‹domain› — and the braces left in the configuration below — is filled in with your own values when this rule appears on a report.
Remediation by platform
# http scope. The map is the allow-list: an origin that is not a key here gets "".
map $http_origin $cors_origin {
default "";
"https://app.example.com" "https://app.example.com";
"https://admin.example.com" "https://admin.example.com";
}
# Credentials are granted only when the origin matched.
map $cors_origin $cors_credentials {
default "";
~. "true";
}
server {
location /api/ {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials $cors_credentials always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age "600" always;
add_header Vary "Origin" always;
if ($request_method = OPTIONS) {
return 204;
}
}
}<IfModule mod_headers.c>
# The capture group is the allow-list: only these exact origins set the variable.
SetEnvIf Origin "^(https://app\.example\.com)$" CORS_ORIGIN=$1
SetEnvIf Origin "^(https://admin\.example\.com)$" CORS_ORIGIN=$1
Header always set Access-Control-Allow-Origin "%{CORS_ORIGIN}e" env=CORS_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS" env=CORS_ORIGIN
Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=CORS_ORIGIN
Header always set Access-Control-Max-Age "600" env=CORS_ORIGIN
# Unconditional: the response varies by Origin whether or not this one matched.
Header always append Vary Origin
</IfModule>@allowed_origin header Origin https://app.example.com
header @allowed_origin {
Access-Control-Allow-Origin "https://app.example.com"
Access-Control-Allow-Credentials "true"
Access-Control-Allow-Methods "GET, POST, OPTIONS"
Access-Control-Allow-Headers "Authorization, Content-Type"
Access-Control-Max-Age "600"
}
# Unconditional, so caches key on Origin even for the unmatched response.
header Vary Origin
@preflight method OPTIONS
respond @preflight 204const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://admin.example.com',
]);
app.use((req, res, next) => {
// Set unconditionally: the answer depends on Origin even when it is refused.
res.vary('Origin');
const origin = req.headers.origin;
if (origin !== undefined && ALLOWED_ORIGINS.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
res.setHeader('Access-Control-Max-Age', '600');
}
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});Rules → Overview → Create rule → Response Header Transform Rule (path as at 2026-09), with an expression that tests the request Origin against a fixed list, e.g.
http.request.headers["origin"][0] in {"https://app.example.com" "https://admin.example.com"}
and a matching rule that always sets Vary: Origin.The
maphas to be at http scope — nginx has no allow-list construct insidelocation, and building one out ofifthere is the source of most broken nginx CORS configurations. nginx skips anadd_headerwhose variable is empty, so an unmatched origin simply receives no CORS headers.alwaysis required or the headers vanish on error responses and on the 204 preflight. Remember that anyadd_headerinside alocationdiscards everyadd_headerinherited from the server block, so keep the whole set together.Shown because the response identified Apache.
SetEnvIfplusenv=is the mechanism —Header sethas no conditional form of its own, and anchoring the regex with^…$is what stopshttps://app.example.com.attacker.examplematching. Put it in the virtual host: .htaccess runs too late for responses the server generates itself, including the preflight.The
header Origin <value>matcher compares the whole value, so it is an exact-match allow-list rather than a prefix test. Repeat the matcher and block for each additional origin.The origin is written back only after the
Setmembership test, which is the difference between an allow-list and a reflection. Never build the set from a request value, an environment variable that is not reviewed, or a substring/endsWithtest —https://app.example.com.attacker.exampleends with nothing useful but starts with everything an attacker needs.Treat this as a sketch of the approach rather than a copy-paste: the exact expression and the dynamic-value syntax differ between Cloudflare plans, and a static-value rule can only serve one origin. A Transform Rule also runs at Cloudflare's edge, so an origin that is reachable directly — by IP, or through a DNS record that is not proxied — still serves the policy measured here. Setting the allow-list at the origin covers both paths and is what we recommend.
Technical detail
Access-Control-Allow-Headers: ‹headers› on a policy whose Access-Control-Allow-Origin is a wildcard or reflected. ‹detail›
As with the method grant, this is only reported in combination with an origin policy that does not narrow anything. Access-Control-Allow-Headers: * against a fixed allow-list of your own front ends is unremarkable.
One honest correction to the usual reading of *: it does **not** cover Authorization. The Fetch Standard excludes it from the wildcard specifically, so a preflight that answers * still fails for a request carrying an Authorization header — you have to name Authorization explicitly. That cuts both ways here. It means a wildcard is not quite as broad as it looks, and it means a policy relying on * for an authenticated API is quietly broken. * is also ignored altogether for credentialed requests, where every header must be named.
A header allow-list is not a security boundary in itself; it is a statement of what the endpoint expects. Treat any header your application trusts as attacker-controlled regardless of what this policy says, and fix the origin policy as the actual remediation. Detected server: ‹detected server›.
Standards and references
Test this on your domain
Run the check that produces this finding, on its own, against any domain.
Other web cors checks
- The Access-Control-Allow-Origin value is not a valid origin
- The CORS policy grants broad methods on an unrestricted origin
- The null origin is on the CORS allow-list
- Any website is allowed to read this site's responses
- Any website can read authenticated responses from this site
- Preflight results are not cached
- The CORS response varies by origin but is not marked Vary: Origin
- Resources are shared with every origin, without credentials
- The CORS policy pairs a wildcard origin with credentials