How does the ‘Access-Control-Allow-Origin’ header work in cross-origin resource sharing (CORS)?
I’ve been trying to understand how to properly implement CORS headers to allow JavaScript code from one domain to access resources on another domain. My current understanding is:
- A client downloads JavaScript code MyCode.js from http://siteA (the origin).
- The response header of MyCode.js contains Access-Control-Allow-Origin: http://siteB, which I thought meant that MyCode.js was allowed to make cross-origin references to site B.
- The client triggers functionality in MyCode.js, which makes requests to http://siteB.
However, this approach doesn’t work as expected. I’ve read about cross-origin resource sharing on Wikipedia and the W3C recommendation, but I’m still unclear on how to properly use this header.
I have control over both site A and site B. How do I enable JavaScript code downloaded from site A to access resources on site B using the Access-Control-Allow-Origin header?
Note: I prefer not to use JSONP as a solution.
The Access-Control-Allow-Origin header works by indicating which origins are permitted to access a resource from a different domain. When a browser makes a cross-origin request, the server responds with this header to specify which domains can receive the response. The header must be set by the server hosting the resource (siteB), not by the server providing the JavaScript code (siteA). This means siteB needs to include Access-Control-Allow-Origin: http://siteA in its responses to allow siteA’s JavaScript to access its resources.
Contents
- Understanding the Access-Control-Allow-Origin Header
- How CORS Actually Works
- Preflight Requests and the OPTIONS Method
- Proper Implementation Steps
- Common Mistakes and Solutions
- Security Considerations
Understanding the Access-Control-Allow-Origin Header
The Access-Control-Allow-Origin header is an HTTP response header that tells browsers whether a resource can be shared with the requesting code from different origins. When you control both siteA and siteB, you need to configure siteB to include this header in its responses.
The header can have three possible values:
Access-Control-Allow-Origin: *- Allows any origin to access the resourceAccess-Control-Allow-Origin: http://siteA- Only allows requests from a specific originAccess-Control-Allow-Origin: null- Used for opaque responses
A critical security restriction is that when credentials are involved, the server cannot use the wildcard (*) and must specify an explicit origin source.
Key Point: The Access-Control-Allow-Origin header is set by the server that hosts the resource being requested (siteB), not by the server that provides the JavaScript code (siteA). This is why your current approach doesn’t work.
How CORS Actually Works
Cross-Origin Resource Sharing (CORS) is a mechanism that allows browsers to make requests to different origins than the one that served the web page. Here’s the correct flow:
- SiteA serves JavaScript code to the client
- The JavaScript code makes a request to siteB
- SiteB’s server responds with the appropriate CORS headers
- The browser checks if the origin is allowed
- If allowed, the browser makes the response available to the JavaScript code
According to Wikipedia, “When performing certain types of cross-domain Ajax requests, modern browsers that support CORS will initiate an extra ‘preflight’ request to determine whether they have permission to perform the action.”
The browser automatically handles the CORS security checks. If siteB doesn’t include the proper Access-Control-Allow-Origin header, the browser will block the response from being accessed by the JavaScript code, even though the HTTP request succeeded.
Preflight Requests and the OPTIONS Method
For certain types of requests, browsers send a preflight request using the OPTIONS method before making the actual request. This happens when:
- The request uses methods other than GET, HEAD, or POST
- The request uses POST with Content-Type other than
application/x-www-form-urlencoded,multipart/form-data, ortext/plain - The request includes custom headers
A preflight request includes headers like:
Origin: The requesting originAccess-Control-Request-Method: The HTTP method to be usedAccess-Control-Request-Headers: (Optional) Custom headers to be sent
The server must respond to preflight requests with headers indicating what’s allowed:
Access-Control-Allow-Origin: http://siteA
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
As explained by PortSwigger Web Security Academy, “When a CORS request is received, the supplied origin is compared to the whitelist. If the origin appears on the whitelist then it is reflected in the Access-Control-Allow-Origin header so that access is granted.”
Proper Implementation Steps
To enable JavaScript from siteA to access resources on siteB:
1. Configure siteB’s Server Responses
SiteB must include the appropriate CORS headers in its HTTP responses:
Access-Control-Allow-Origin: http://siteA
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
2. Handle Preflight Requests
SiteB must respond to OPTIONS requests with the appropriate headers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://siteA
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
3. Special Cases for Credentials
If you need to send cookies or authentication headers, you must:
- Include
Access-Control-Allow-Credentials: true - NOT use the wildcard (
*) forAccess-Control-Allow-Origin - Specify the exact origin
Example:
Access-Control-Allow-Origin: http://siteA
Access-Control-Allow-Credentials: true
4. Server-Side Implementation Examples
Here are implementations for different server types:
Node.js Express
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://siteA');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
app.options('*', (req, res) => {
res.header('Access-Control-Allow-Origin', 'http://siteA');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.send();
});
Nginx Configuration
server {
listen 80;
server_name siteB.com;
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'http://siteA';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' 'http://siteA' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
# Your existing configuration
}
}
Common Mistakes and Solutions
Mistake 1: Setting the Header on the Wrong Server
Problem: Setting Access-Control-Allow-Origin on siteA instead of siteB
Solution: The header must be in responses from siteB, the server being requested
Mistake 2: Using Wildcard with Credentials
Problem: Using Access-Control-Allow-Origin: * when sending cookies or auth headers
Solution: Use specific origins when credentials are involved
Mistake 3: Missing Preflight Handling
Problem: Not responding to OPTIONS requests
Solution: Implement OPTIONS handler on siteB
Mistake 4: Requesting from File://
Problem: CORS doesn’t work when opening HTML files directly from disk
Solution: Use a local server during development
Security Considerations
When implementing CORS, consider these security best practices:
- Principle of Least Privilege: Only allow specific origins that need access
- Avoid Wildcard for Sensitive Data: Never use
*for APIs that handle sensitive information - Validate Origins: Ensure the origin in requests matches what you expect
- Consider Time Limits: Set appropriate
Access-Control-Max-Agevalues for preflight caching - Monitor for Abuse: Keep logs of CORS requests to detect potential security issues
According to PortSwigger, “the cross-domain server can permit reading of the response when credentials are passed to it by setting the CORS Access-Control-Allow-Credentials header to true.”
Sources
- Cross-Origin Resource Sharing (CORS) - HTTP | MDN
- Access-Control-Allow-Origin header - HTTP | MDN
- Cross-origin resource sharing - Wikipedia
- CORS and the Access-Control-Allow-Origin response header | Web Security Academy
- Enable Cross-Origin Requests (CORS) in ASP.NET Core | Microsoft Learn
- CORS, Preflight request and OPTIONS Method - DEV Community
- Understanding CORS and Preflight Requests in APIs | Medium
Conclusion
To properly enable JavaScript from siteA to access resources on siteB using CORS:
- Configure siteB to include
Access-Control-Allow-Origin: http://siteAin its responses - Handle preflight requests with proper OPTIONS method responses
- Avoid using wildcard (*) when credentials are involved
- Test thoroughly with different request types and headers
- Follow security best practices to maintain proper access controls
Your initial approach was incorrect because you were setting the CORS header on siteA instead of siteB. Remember that the server being requested (siteB) is responsible for indicating which origins can access its resources. By implementing the correct headers on siteB, you’ll enable cross-origin requests while maintaining security through the browser’s CORS enforcement mechanism.