Developers are primarily familiar with CORS due to the errors they frequently encounter as a result of it. Chances are, you landed here because you, too, are experiencing a CORS issue that you need to understand and resolve. CORS, or Cross-Origin Resource Sharing, is a browser security feature that controls how web pages can request resources from domains other than their own. This mechanism protects users and servers from potentially malicious cross-site access, but it can also trip up developers when building modern applications.
In this article, we’ll explain CORS, show how it works under the hood, and offer best practices for configuring it securely, whether you’re building a front-end app or securing APIs.
What Is CORS and Why Does It Exist?
Cross-Origin Resource Sharing (CORS) is a mechanism that allows web applications to make requests to domains different from the one serving the original web page. CORS policies are defined on the server, but they are enforced by the browser. CORS hasnโt always existed, and to understand why CORS exists, we need to first understand what came before it.
The Same-Origin Policy Foundation
Before CORS, browsers enforced a strict “Same-Origin Policy” that dictated:
- Scripts loaded from a particular domain could only make requests to that same domain
- This prevented
www.example.com
from making requests toapi.differentsite.com
While secure, this limitation made it difficult to build modern web applications, which typically require multiple servers and domains to operate. Therefore, this approach quickly became obsolete.
Why CORS Became Necessary
As web applications evolved, developers needed legitimate ways to:
- Host static content at
www.example.com
and backend APIs atapi.example.com
- Integrate with third-party services and APIs
- Build microservices architectures with multiple domains
Unlike the days of serving applications on a single server and a single origin (or domain), these applications required the ability to handle a multi-origin approach. This is where CORS came in to offer a controlled way to relax the Same-Origin Policy, enabling cross-origin requests while maintaining security through explicit permission mechanisms.
An important note is that since CORS is enforced at the browser level, using tools like Postman to test an API request or resources on the server doesnโt get caught up in CORS. This explains why many developers can test their API endpoint without issue in these types of tools and then encounter CORS issues when using the endpoint in their browser-based application.
How Does CORS Work? A Step-by-Step Breakdown
Understanding how CORS works is crucial for both developers and security professionals. The CORS workflow involves a “preflight” process that validates cross-origin requests before they’re executed.
The CORS Preflight Process
Here’s exactly what happens when a cross-origin request is made:
- JavaScript attempts a cross-origin request from
frontend.example.com
toapi.example.com
- The browser automatically sends an OPTIONS preflight request to the target server
- The server responds with CORS headers indicating what’s allowed
- The browser decides whether to allow the actual request based on the server’s response
Real-World CORS Example
Building on the principles from above, letโs look at a real-world example where a front-end application, running in a browser, makes a request to a /users API that sits on another domain, like so:
Origin: https://frontend.example.com
Request: POST https://api.example.com/users
The browser first sends a pre-flight check, letting the server know the origin of the request and the endpoint it is trying to perform the POST request on:
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
The server then responds with a pre-flight response, which lets the browser know which origins, via the Access-Control-Allow-Origin header, are allowed (and the methods as well, in this case):
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type
Once the browser receives this response and verifies that the origin, method, and headers are permitted, it will proceed with the actual POST request. If the origin isnโt permitted, the browser will then block it and show an error message in the browser’s console.
It is essential to note, however, that not all cross-origin requests require a preflight. Simple requests, such as GET or POST with standard headers and content types like text/plain
or application/x-www-form-urlencoded
, are sent directly without a preflight check.
CORS Request Headers Explained
Before an actual request can be sent, the preflight request must be sent from the browser to the target server. When browsers make preflight requests, they include specific headers that servers must validate:
Origin Header
- Purpose: Identifies the domain making the request
- Example:
Origin: https://frontend.example.com
- Security Note: Should always be validated against an allowlist
Access-Control-Request-Method
- Purpose: Specifies the HTTP method for the actual request
- Example:
Access-Control-Request-Method: POST
- Best Practice: Validate against supported API methods
Access-Control-Request-Headers
- Purpose: Lists custom headers the request wants to send
- Example:
Access-Control-Request-Headers: Authorization, Content-Type
- Reliability: Not always reliable, as actual requests may vary
CORS Response Headers Explained
Once the server processes the request, it then sends a response back to the browser that made the request. Server responses include headers that define what cross-origin requests are permitted, such as:
Access-Control-Allow-Origin
- Purpose: Specifies which origins can access the resource
- Valid Values: Specific origin or
*
(wildcard) - Security Critical: Must match request origin exactly, wildcard not recommended in production environments
Access-Control-Allow-Methods
- Purpose: Lists permitted HTTP methods
- Example:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
- Best Practice: Only include methods your API actually supports
Access-Control-Allow-Headers
- Purpose: Specifies allowed request headers
- Example:
Access-Control-Allow-Headers: Authorization, Content-Type
- Usage: Should reflect preflight request headers or use a wildcard
Access-Control-Allow-Credentials
- Purpose: Boolean indicating if credentials (cookies, auth headers) are allowed
- Values:
true
or omitted (defaults tofalse
) - Security Note: Cannot be
true
whenAccess-Control-Allow-Origin
is*
Access-Control-Max-Age
- Purpose: Specifies how long browsers should cache preflight responses
- Example:
Access-Control-Max-Age: 3600
(1 hour) - Benefit: Reduces preflight request overhead
Access-Control-Expose-Headers
- Purpose: Specifies which response headers client scripts can access
- Example:
Access-Control-Expose-Headers: X-Custom-Header
- Default: Only basic headers are exposed to scripts
The browser then processes the values within these headers and enforces the CORS policy based on them. If the request from the browser is not permitted, as indicated by these headers, it will not be sent, and an error will be thrown within the browser.
CORS Best Practices for Secure Implementation
As mentioned, configuring CORS properly, without using insecure or development-only workarounds, means adding or changing code on the server itself where the request originates. At the server level, implementing CORS securely requires careful attention to configuration details. Letโs look at a few ways that this could be done on a NodeJS server that can be easily rolled out to other languages and frameworks in a similar fashion, and a few things you should not do.
Validate Origins Against a Whitelist
One of the most common ways to create a CORS configuration that will work well and support multiple domains is to use an origin whitelist. In the code below, you will see that weโve created an array of allowed origins and then compare the request origin (the origin in the browser) to those in the whitelist. If there is a match, the Access-Control-Allow-Origin
header will be returned with that value, and the browser will then send the actual request since the preflight checks have passed.
const allowedOrigins = [
'https://frontend.example.com',
'https://app.example.com',
'https://admin.example.com'
];
if (allowedOrigins.includes(requestOrigin)) {
response.setHeader('Access-Control-Allow-Origin', requestOrigin);
}
Never Use Wildcards with Credentials
Often, when working on local development, developers set the Access-Control-Allow-Origin
header to the wildcard (*
) value. While this can be fine for local development, this will allow every origin to make requests to the server, and in doing so, will completely eliminate any security that CORS would offer. So, in short:
- Wrong:
Access-Control-Allow-Origin: *
withAccess-Control-Allow-Credentials: true
- Right: Validate origin and reflect specific domain when credentials are needed
Use Secure String Comparison
When creating your CORS logic and using string comparisons within the logic, youโll want to do the following:
- Avoid regex patterns that can be bypassed
- Instead, use exact string matching for origin validation
- Consider subdomain policies carefully
Implement Logging and Alerting
Logging of CORS events and subsequent alerting is also a smart idea to add to your server-side logic. Although some blocked requests may not be malicious, others could be. When a CORS request is blocked, it should be logged on the server. You can then use your preferred log parsing tool to alert your development or security team to a potential security risk that is unfolding. Hereโs an example of what that may look like in NodeJS:
if (!allowedOrigins.includes(requestOrigin)) {
console.log(`Blocked CORS request from: ${requestOrigin}`);
// Alert security team for potential attack
}
Common CORS Misconfigurations and Security Risks
Although CORS may cause headaches for developers at certain points in the development lifecycle, it does serve an important purpose. Understanding how CORS security vulnerabilities work helps prevent dangerous misconfigurations. Letโs take a deeper look at some insecure misconfigurations to be aware of:
The Wildcard Danger
Setting Access-Control-Allow-Origin: *
effectively disables the Same-Origin Policy:
- Risk: Any malicious site can make requests to your API
- Impact: Potential data theft and unauthorized actions
- Solution: Always validate and reflect specific origins
Blind Origin Reflection
Reflecting the Origin
header without validation is worse than wildcards:
// DANGEROUS - Don't do this!
response.setHeader('Access-Control-Allow-Origin', request.headers.origin);
Why it’s dangerous: It bypasses browser protections and allows credentials to be used with any origin.
Real-World Attack Scenario
CORS attacks do occur, and they can be challenging to detect, especially for users who unknowingly use the compromised application in their browser. Here is how such an attack may unfold:
- The victim visits a malicious site (through phishing or a malicious ad)
- Evil JavaScript runs on the malicious page
- The script makes requests to a vulnerable API with the victim’s cookies
- API responds due to misconfigured CORS
- Attacker steals data or performs unauthorized actions
Of course, CORS is one piece of the security puzzle, and the above scenario likely isnโt quite as simple to execute as weโve made it seem. But that said, if the application has multiple security issues, having CORS as a frontline defense could help stop this attack in its tracks.
How to Securely Enable CORS
Almost every framework and language that supports server-side development has a way to easily implement and handle CORS. Letโs take a look at a code example, some popular frameworks that make handling CORS easy, and some testing and validation tips.
Validated Origin Reflection Example
Looking further at an example of how to implement CORS policies in JavaScript. Again, using your language or framework of choice, you can transcribe this same pattern to work effectively. Here is an example of a setCORSHeaders
function that takes the request’s origin, compares it to a whitelist, and if it exists, then reflects the validated origin back to the browser in the response.
function setCORSHeaders(request, response) {
const origin = request.headers.origin;
const allowedOrigins = [
'https://frontend.example.com',
'https://app.example.com'
];
if (allowedOrigins.includes(origin)) {
response.setHeader('Access-Control-Allow-Origin', origin);
response.setHeader('Access-Control-Allow-Credentials', 'true');
} else {
response.status(403).send('Forbidden');
}
}
Framework Libraries and Reverse Proxies
When configuring CORS, many libraries exist to simplify these configurations (and also help with secure configurations), as well as the ability to configure it on an API gateway or secure proxy, such as NGINX.
Across different frameworks, some of the most popular CORS libraries include:
- Express.js:
cors
middleware - Django:
django-cors-headers
- Spring Boot:
@CrossOrigin
annotation - Laravel: Built-in CORS middleware
When configuring this at the gateway or reverse proxy level, this can be done in the proxy’s configuration or, in some cases, via UI. In the case of NGINX, here is how simple this type of configuration is to implement:
location /api {
if ($http_origin = "https://frontend.example.com") {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Credentials true;
}
}
Testing and Validation Tips
Regardless of how you decide to configure your CORS policies, here are a couple of ways you can test and validate it to make sure it matches the expected behaviour and is secure:
- Use browser dev tools to inspect CORS headers
- Test with different origins to ensure validation works
- Verify preflight responses match your expectations
- Check credential handling with authentication
How StackHawk Can Help with CORS and API Security
While proper CORS configuration is essential, it’s just one piece of your application security puzzle. Even with CORS properly configured, your APIs may still be vulnerable to other security issues.
StackHawk’s dynamic application security testing (DAST) helps identify security vulnerabilities, such as CORS Misconfiguration, that can affect your APIs.
On top of this, it can also help identify other critical vulnerabilities such as:
- Cross-Site Request Forgery (CSRF): Protects against malicious sites forcing authenticated actions
- Cross-Site Scripting (XSS): Detects code injection vulnerabilities that could bypass CORS protections
- Authentication flaws: Identifies weak authentication mechanisms that CORS relies on
- API-specific vulnerabilities: Finds issues unique to REST and GraphQL APIs
Key Benefits:
- Automated scanning in CI/CD pipelines
- Developer-friendly remediation guidance
- Comprehensive vulnerability detection beyond CORS
- Integration with existing security tools
Getting Started with StackHawk
To start testing applications for CORS vulnerabilities with StackHawk, youโll need an account. You canย sign up for a trial account. If youโre using an AI-coding assistant like Cursor or Claude Code, sign up for our $5/month single-user plan,ย Vibe, to find and fix vulnerabilities 100% in natural language.
Configuring CORS in Your Stack: Framework-Specific Guides
Whether you’re encountering CORS errors in development or need to configure production-ready CORS policies, we’ve created detailed guides for the most popular tools and frameworks:
Front-End CORS Configuration:
- Angular CORS – Handle cross-origin requests in Angular applications
- TypeScript CORS – Type-safe CORS handling patterns
- React CORS – Resolve common CORS issues in React apps
- Vue.js CORS – Configure Vue applications for cross-origin requests
Server-Side CORS Configuration:
- Node.js CORS – Express.js and other Node frameworks
- Django CORS – Python web framework implementation
- Golang CORS – Secure CORS in Go applications
- FastAPI CORS – Python async framework configuration
- .NET CORS – ASP.NET Core implementation
- Spring Boot CORS – Java framework configuration
- Rails CORS – Ruby on Rails setup
- Rust CORS – Secure CORS in Rust web frameworks
- Kotlin CORS – JVM-based implementation
- Laravel CORS – PHP framework implementation