Join our Newsletter — 33% off our NHI Course
Home› Guides› CSP vs CORS: What Each Browser Control Actually…
Guide Web & API Security

CSP vs CORS: What Each Browser Control Actually Protects

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 14 November 2026 17 min read
On this page

Content Security Policy and Cross-Origin Resource Sharing are both response headers the browser enforces, and teams regularly deploy one believing it does the job of the other. CSP restricts what your own page is allowed to load and execute, so it protects your users from content injected into your origin. CORS decides which other origins may read responses from your server, so it loosens the same-origin policy on purpose. One narrows, the other widens, and they point in opposite directions. This guide compares them as controls: the threat each one addresses, how each works in the browser, how each fails, how to test them, and how to decide what each application needs. It is written for application security engineers, front-end and API leads, and assessors reviewing header configurations. For directive-level CSP detail see the Content Security Policy Guide; for CORS flaws in depth see the CORS Misconfiguration Guide.

Key takeaways

  • Opposite directions. CSP is a restriction your page places on itself (what it may load, run, frame and post to); CORS is a permission your server grants to other origins to read its responses.
  • CORS does not stop XSS, and CSP does not protect APIs. CORS has no effect on script injected into your own origin; CSP on an HTML page does nothing to stop another site reading a JSON API that returns permissive CORS headers.
  • CORS governs reading, not sending. The Fetch Standard describes CORS as headers that indicate "whether a response can be shared cross-origin". Simple cross-origin requests are still sent, so CSRF defences remain necessary.
  • The worst misconfigurations are permissive by design. For CORS: reflecting any Origin with credentials, allowlisting null and unanchored pattern matches. For CSP: 'unsafe-inline', broad host allowlists and a policy left in Report-Only mode for good.
  • Both are defence in depth. The CSP Level 3 draft says CSP "is not intended as a first line of defense"; OWASP warns the Origin header can be spoofed outside the browser. Output encoding and server-side authorisation still do the primary work.

The short answer

Deploy CSP on every HTML response your application serves, aiming for a strict nonce-based or hash-based policy with object-src 'none', base-uri 'none' and an explicit frame-ancestors. Its job is to limit the damage when an injection flaw slips through. Configure CORS only on endpoints that genuinely need to be read by a browser running on another origin, with an exact-match allowlist of origins, and leave it off everywhere else. Its job is to grant the narrowest possible exception to the same-origin policy. If you are asking "which one should we use", the question is usually mistaken: most web applications need a strict CSP, and many need no CORS at all. Read the Same-Origin Policy and Browser Security Model Guide first if the baseline rules are unfamiliar.

Side by side

DimensionContent Security Policy (CSP)Cross-Origin Resource Sharing (CORS)
Effect on the same-origin policyAdds restrictions beyond itRelaxes it for named origins
Who is protectedUsers of your page, against content running in your originYour server's data, against being read by pages on other origins
Set onHTML documents (and workers)Resources read by cross-origin scripts, usually APIs, fonts, data files
Main headersContent-Security-Policy, Content-Security-Policy-Report-Only, Reporting-EndpointsAccess-Control-Allow-Origin, -Allow-Credentials, -Allow-Methods, -Allow-Headers, -Expose-Headers, -Max-Age, plus Vary: Origin
Default when absentNo extra restriction: the page loads whatever its markup asks forMost restrictive: cross-origin scripts cannot read the response
Attacks addressedXSS (CAPEC-63), clickjacking (CAPEC-103) via frame-ancestors, data exfiltration via connect-src and form-action, mixed contentNone directly: it is an access grant. Misconfiguration creates cross-origin data theft (CWE-942)
Typical failurePolicy too loose to stop an injected scriptPolicy so loose that an attacker's site can read authenticated responses
Testing referenceOWASP WSTG-CONF-12OWASP WSTG-CLNT-07
ASVS 5.0 requirementsV3.4.3, V3.4.6, V3.4.7V3.4.2, V3.5.1, V3.5.2
SpecificationW3C CSP Level 3 (Working Draft, 27 October 2026, checked 14 November 2026)WHATWG Fetch Standard, CORS protocol section (Living Standard)

The "default when absent" row is the most useful single fact in this table. Missing CSP is a lost safety net; missing CORS is usually the safe state. That asymmetry is why a header scanner that reports "CORS not configured" as a finding is wrong, and one that reports "CSP not configured" is right.

CSP: threat model and architecture

Threat model: CSP assumes an attacker has found a way to put markup or script into a page served from your origin: a stored or reflected XSS flaw, a DOM sink fed from the URL, a compromised third-party script, or an injected <base> tag that redirects relative script loads. Once injected code runs in your origin it has your users' session, your DOM and your same-origin API access, which is why ATT&CK lists outcomes such as T1185 Browser Session Hijacking and T1189 Drive-by Compromise. CSP does not find or fix the injection. It reduces what the injected content can do.

Architecture in words: the server sends a policy with the document. The browser parses it into directives, and before every fetch, script execution, frame load or form submission it checks the action against the relevant directive. Fetch directives (script-src, connect-src, img-src and others, falling back to default-src) control what loads. Document directives such as base-uri constrain the page itself. Navigation directives (form-action, frame-ancestors) control where the page can submit and who can frame it. Violations are blocked and, if report-to names an endpoint declared in Reporting-Endpoints, reported as csp-violation reports.

Strict versus allowlist: Google's strict CSP guidance states that URL allowlist policies need heavy customisation and can be bypassed in most configurations, because any allowlisted host that serves JSONP endpoints or user-controlled files becomes a script gadget. A strict policy instead trusts scripts by a per-response nonce or by hash:

  • Nonce-based: script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none', for server-rendered pages. The nonce must be random and unique per response, and OWASP advises applying it through the templating engine rather than middleware that tags every script.
  • Hash-based: the same policy with SHA-256 hashes of each trusted inline script, for static or cached pages. Any change to the script, including whitespace, changes the hash.
  • Fallbacks: https: and 'unsafe-inline' may be added for very old browsers; browsers that support nonces and 'strict-dynamic' ignore them.

Beyond scripts: frame-ancestors replaces X-Frame-Options for clickjacking defence, which ASVS 5.0 V3.4.6 now describes as obsolete. require-trusted-types-for 'script' makes DOM injection sinks reject plain strings; see the Trusted Types and DOM XSS Prevention Guide. upgrade-insecure-requests rewrites HTTP subresource loads to HTTPS but does not replace HSTS.

CORS: threat model and architecture

Threat model: CORS exists because the same-origin policy blocks legitimate designs, such as a single-page application on app.example.com calling an API on api.example.com. The threat it introduces, rather than mitigates, is a malicious page on another origin using a victim's browser, and the victim's cookies or ambient network position, to read data the attacker could not fetch directly. The Fetch Standard warns that "extreme care has to be taken to avoid the confused deputy problem" when sharing credentialed responses. The browser is the deputy, acting with the user's authority on behalf of whichever page is open.

Architecture in words: a script on origin A issues fetch() or XMLHttpRequest to origin B. The browser attaches an Origin header. For a simple request (GET, HEAD or POST, only CORS-safelisted headers, and a Content-Type of application/x-www-form-urlencoded, multipart/form-data or text/plain) the browser sends the request immediately and then checks the response for Access-Control-Allow-Origin before letting the script read it. Anything else, such as a PUT, a DELETE, a custom header or a JSON content type, triggers an OPTIONS preflight carrying Access-Control-Request-Method and Access-Control-Request-Headers; the real request is sent only if the preflight response permits it. Preflight results may be cached for Access-Control-Max-Age seconds, subject to a browser-imposed ceiling.

Credentials rules: for a credentialed request the server must name an explicit origin, not *, in Access-Control-Allow-Origin, must send Access-Control-Allow-Credentials: true (the Fetch Standard requires exactly that value), and cannot use * in the allow-methods, allow-headers or expose-headers lists. When the allowed origin is chosen dynamically from a list, the response must carry Vary: Origin so shared caches do not serve one origin's grant to another. Third-party cookie restrictions in modern browsers can block credentialed cross-site requests even when the server permits them, which is a reason to prefer token-based authentication for genuinely cross-site APIs; see Session Cookies vs Bearer Tokens.

What CORS is not: OWASP's HTML5 Security Cheat Sheet states that "CORS does not prevent the requested data from going to an unauthorized location" and that standard CSRF prevention is still needed. The request has already reached your server by the time the browser decides whether the script may read the answer. Outside a browser, Origin is just a header any client can set, so CORS is never an authorisation control for non-browser callers.

Where each one breaks

Failure modeControlWhat happensFix
'unsafe-inline' in script-src without a nonce or hashCSPInjected inline scripts and event handlers run; the policy blocks little that mattersMove inline code to files or nonce it; replace onclick attributes with addEventListener
Broad host allowlist (https:, *.cdn.example, public CDNs)CSPAny JSONP endpoint or user-uploaded file on an allowed host becomes an execution routeNonce or hash with 'strict-dynamic'; keep host lists short and reviewed
Missing object-src and base-uriCSPPlugin content or an injected <base> tag sidesteps script restrictionsSet both to 'none', as ASVS V3.4.3 requires at minimum
Report-Only forever, or frame-ancestors in a meta tagCSPNothing is enforced; meta-delivered policies ignore frame-ancestors, report-uri and sandboxSet an enforcement date; deliver policy as a response header
Static or reused nonceCSPAn attacker who reads one page can reuse the nonce in injected markupGenerate a cryptographically random nonce per response; never cache nonced HTML
Origin reflection with credentialsCORSAny site the victim visits can read authenticated API responsesExact-match allowlist; never echo Origin unchecked
Unanchored pattern matchCORSA check for "contains example.com" accepts example.com.attacker.testCompare the full serialised origin (scheme, host, port) against a list
null origin allowlistedCORSSandboxed iframes and some redirects send Origin: null, which an attacker can produce at willNever allowlist null
* on a response carrying sensitive dataCORSData protected only by network position (intranet, VPN, IP allowlist) is readable from any site a user visitsReserve * for truly public, unauthenticated data, as ASVS V3.4.2 requires
Missing Vary: OriginCORSA cache stores one origin's grant and serves it to others, or breaks legitimate callersSend Vary: Origin whenever the allowed origin is computed

A pattern links the two columns: both controls fail by trusting too much, and both usually loosen under delivery pressure. A developer adds 'unsafe-inline' to make a widget work, or reflects the Origin header to stop a console error, and the change survives because nothing breaks.

How CSP and CORS interact

  • connect-src limits your page; CORS limits theirs. For your page's script to read a cross-origin API, both must agree: your CSP connect-src must allow the API origin, and the API's CORS headers must allow your page's origin. A failure on either side shows up as a console error, and teams sometimes "fix" the wrong one.
  • XSS defeats CORS allowlists. If app.example.com is on your API's CORS allowlist and suffers XSS, the injected script runs as that trusted origin. Every origin on a credentialed allowlist is effectively part of your API's attack surface, and a strict CSP on those origins protects the API too.
  • CSP can contain exfiltration. A tight connect-src, img-src and form-action limits where stolen data can be sent from a compromised page. This is one of CSP's main roles against web skimming; see the Web Skimming Incident Playbook.
  • Preflight is not CSRF protection by default. ASVS 5.0 V3.5.2 requires that, if you rely on preflight to block cross-origin use of sensitive functions, the function cannot be reached with a request that skips preflight, for example by checking Content-Type or requiring a non-safelisted header. V3.5.1 requires anti-forgery tokens or equivalent where you do not rely on preflight. See the CORS vs CSRF Protection guide.

Worked scenario

A retailer runs a server-rendered storefront on www.shop.example, a single-page account area on account.shop.example and a JSON API on api.shop.example using session cookies. A partner site partner.example displays public stock levels. An assessment finds three things.

  1. CORS on the API reflects any Origin with credentials. This lets any site a logged-in customer visits read their order history. The fix is an allowlist of exactly https://account.shop.example for credentialed routes, Vary: Origin, and a separate unauthenticated /public/stock route that returns Access-Control-Allow-Origin: * with no cookies and no personal data for the partner.
  2. The storefront CSP is script-src 'self' 'unsafe-inline' https:. This would not stop a stored XSS in product reviews. The team ships a Report-Only strict policy with nonces generated in the template layer, triages violations for two release cycles, refactors 40 inline handlers, then enforces. connect-src is limited to the API and the analytics origin, and form-action 'self' stops injected forms posting card data elsewhere.
  3. The account area has no frame-ancestors. Adding frame-ancestors 'none' closes a clickjacking route on the "change email" page; see the Clickjacking Guide.

The lesson is the interaction: the CORS fix narrows who can read the API, and the CSP fix protects the one origin still trusted to read it. The numbers here are illustrative.

Decision criteria

SituationCSPCORS
Server-rendered site, same-origin APIStrict nonce policyNone needed
Static SPA from CDN calling API on another subdomainHash-based strict policy; connect-src for the APIExact-origin allowlist; credentials only if cookies are used
Public open data APINot relevant to JSON responses beyond a restrictive default* without credentials, no sensitive fields
Partner integrations reading customer dataStrict policy on every allowlisted origin you controlPrefer server-to-server calls with tokens; if browser-based, exact allowlist per partner, reviewed
Internal tools on a corporate networkStrict policyNone, and never *: network position is not authentication
Heavy third-party tag estate (marketing scripts)Nonce with 'strict-dynamic', accepting that trust propagates to loaded scriptsNot relevant

Small versus large organisations: a small team gets most of the value from a framework's built-in CORS middleware configured with a literal origin list and a hash-based CSP generated at build time. Large estates need central ownership: a shared CORS configuration library with tests that reject reflection and null, a CSP reporting pipeline with violation triage, and policy templates per application type.

Rolling out and testing

  1. Inventory: list every endpoint that returns Access-Control-Allow-* headers and every HTML response with or without CSP. Gateways, CDNs and frameworks often add CORS headers globally, so test the live responses, not the code.
  2. Remove unneeded CORS: delete grants that no browser client uses. Replace reflection with a literal list and add Vary: Origin.
  3. Deploy CSP in Report-Only: send the strict policy as Content-Security-Policy-Report-Only with report-to and, for older browsers, report-uri, which MDN describes as deprecated but still worth declaring alongside.
  4. Refactor: remove inline handlers, javascript: URLs and eval(); check the policy with a CSP evaluator.
  5. Enforce in rings: enforce on low-traffic pages first, keep the Report-Only header alongside for the next, stricter version.
  6. Test continuously: follow WSTG-CLNT-07 and WSTG-CONF-12 in assessments and add automated checks in CI: send crafted Origin values (a foreign domain, a suffix variant, null) and assert that no credentialed grant comes back; parse the CSP and fail the build on 'unsafe-inline' without a nonce, missing object-src or base-uri, or a missing frame-ancestors.
  7. Roll back safely: CSP rollback means reverting to the previous enforced policy while keeping the new one in Report-Only, never removing CSP. CORS rollback means restoring a specific origin to the list, never re-enabling reflection.

Detection and measurement

  • Strict CSP coverage: percentage of HTML responses with an enforced policy containing a nonce or hash, object-src 'none' and base-uri 'none'.
  • Violation trend: CSP violation reports per day by directive and page, split into expected (extensions, legacy code) and suspicious (inline script on a page with user content).
  • Credentialed CORS grants: number of endpoints returning Access-Control-Allow-Credentials: true and the number of origins each allows; this should fall and stay small.
  • Dangerous CORS patterns: count of endpoints that reflect arbitrary origins or accept null, target zero.
  • Report-Only age: days each Report-Only policy has run without enforcement.

Treat a CSP report showing an inline script blocked on a page that renders user content as a possible XSS attempt, not noise. Reports should not carry tokens in URLs, so strip query strings at the collector.

Framework mapping

FrameworkCSPCORS
OWASP ASVS 5.0V3.4.3 CSP with object-src 'none' and base-uri 'none' (L2; per-response nonce or hash at L3); V3.4.6 frame-ancestors (L2); V3.4.7 violation reporting (L3)V3.4.2 fixed or allowlisted Access-Control-Allow-Origin (L1); V3.5.1 and V3.5.2 request forgery and preflight reliance (L1)
OWASP WSTGWSTG-CONF-12WSTG-CLNT-07
CAPECCAPEC-63 Cross-Site Scripting, CAPEC-103 ClickjackingCAPEC-111 JSON Hijacking (cross-origin data reading)
CWECWE-79, CWE-1021CWE-942 Permissive Cross-domain Policy with Untrusted Domains
NIST SP 800-53 Rev. 5SI-10 Information Input Validation (CSP as a compensating layer), SA-11 Developer Testing and EvaluationSA-11, plus application access enforcement for the data behind the API

Common failure patterns

  • Using CORS to "secure" an API: restricting Access-Control-Allow-Origin and calling it access control. Scripts, servers and attackers' tools ignore it; authenticate and authorise every request. See the API Security Guide.
  • Expecting CORS to stop XSS: CORS has no effect on script injected into your own origin. Encode output and deploy CSP; see the Cross-Site Scripting (XSS) Prevention Guide.
  • Copying a framework's permissive default to production: development configurations that allow all origins, or reflect them, ship unchanged.
  • A CSP that lists every vendor's CDN: each new tag adds a host and the policy drifts into an allowlist that blocks nothing an attacker needs.
  • Headers set in two layers: the application and the CDN or gateway both emit CORS or CSP headers, producing duplicates or conflicts that behave differently per browser.
  • Trusting all subdomains: a regex for any subdomain lets a forgotten marketing site, or a subdomain takeover, inherit credentialed API access.

Practitioner checklist

  • Every HTML response sends an enforced CSP header with a nonce or hash, object-src 'none', base-uri 'none' and frame-ancestors.
  • No 'unsafe-inline' or 'unsafe-eval' is relied on in script-src; inline handlers are refactored.
  • Nonces are random per response and nonced HTML is not cached.
  • CSP reporting uses Reporting-Endpoints and report-to, and violations are triaged, not just stored.
  • CORS is enabled only on endpoints with a real cross-origin browser client.
  • Allowed origins are an exact-match list; no reflection, no null, no unanchored patterns, with Vary: Origin set.
  • * is used only on unauthenticated, non-sensitive responses.
  • CSRF protection is in place regardless of CORS settings.
  • CI tests send crafted Origin values and parse CSP on every build.
  • One layer owns each header: application, gateway or CDN, documented.

Standards and references

Related NHI Mgmt Group resources: Security Headers Guide, Content Security Policy Guide, CORS Misconfiguration Guide, Same-Origin Policy and Browser Security Model Guide, XSS vs CSRF.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 14 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org