CSRF and CORS: Securing Browser Requests the Right Way
You've built a web app with a clean API, but then you notice strange requests in your logs—or worse, a security scanner flags your site for CSRF and CORS misconfigurations. These two acronyms often get lumped together, but they solve different problems. Misunderstanding them can leave your users vulnerable or break legitimate cross-origin requests.
In this article, we'll clarify what CSRF and CORS actually are, how they interact with browser security, and give you concrete steps to secure your applications without breaking functionality.
What is CSRF and Why Should You Care?
Cross-Site Request Forgery (CSRF) is an attack that tricks a user's browser into sending a request to a site where they're authenticated. Imagine you're logged into your bank at bank.com. You then visit a malicious site that contains an image tag like <img src="https://bank.com/transfer?to=attacker&amount=1000">. Your browser automatically includes your bank cookies with the request, and if the bank doesn't have CSRF protections, the transfer goes through.
The key point: CSRF exploits the trust a site has in the user's browser. The attacker doesn't need to steal your session; they just need your browser to make a request on your behalf.
How CSRF Attacks Work
For a CSRF attack to succeed, three conditions must be met:
- The victim must be authenticated (e.g., have a valid session cookie).
- The attacker must know the structure of the request (endpoint, parameters).
- The request must not include any unpredictable parameters that the attacker cannot guess.
Common targets are state-changing operations: changing email, transferring funds, posting content, or modifying permissions.
What is CORS and Why Does It Exist?
Cross-Origin Resource Sharing (CORS) is a browser mechanism that allows or denies web pages from making requests to a different domain than the one that served the page. It's an extension of the Same-Origin Policy (SOP), which restricts how a document or script loaded from one origin can interact with resources from another origin.
Without CORS, a malicious site could use JavaScript to read data from your bank's API if you're logged in. CORS gives servers a way to explicitly allow certain cross-origin requests.
The Same-Origin Policy (SOP)
Two URLs have the same origin if the protocol, host, and port are identical. For example, https://example.com/app and https://example.com/api share the same origin, but http://example.com (different protocol) and https://api.example.com (different host) do not.
SOP prevents scripts from one origin from reading responses from another origin. CORS relaxes this by adding HTTP headers that tell the browser whether to allow the request.
CSRF vs CORS: Key Differences
It's crucial to understand that CSRF and CORS are not opposites; they address different security concerns. CSRF is about preventing unauthorized state-changing requests, while CORS is about controlling which origins can read responses.
| Aspect | CSRF | CORS |
|---|---|---|
| Primary goal | Prevent forged requests | Control cross-origin reads |
| Attack vector | Malicious site triggers requests | Malicious site reads responses |
| Defense mechanism | Tokens, SameSite cookies | HTTP headers (Access-Control-*) |
| Browser enforcement | None (server must validate) | Yes (browser blocks reads) |
How to Defend Against CSRF
There are several proven strategies to mitigate CSRF. You don't need all of them, but layering defenses is wise.
1. Use Anti-CSRF Tokens
The most robust defense is to include a unique, unpredictable token in each state-changing request. The server validates the token before processing. Tokens should be tied to the user's session and not exposed in URLs (to avoid leaking via referrer headers).
// Example: Generating and validating a CSRF token in Node.js/Express
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/process', csrfProtection, (req, res) => {
// Token is automatically validated
res.send('OK');
});
2. Set SameSite Cookies
Modern browsers support the SameSite attribute for cookies. Setting it to Lax or Strict prevents the browser from sending cookies on cross-site requests, which blocks many CSRF attacks. Lax allows cookies on top-level navigations (e.g., clicking a link), while Strict blocks all cross-site requests.
Set-Cookie: sessionId=abc123; SameSite=Lax; Secure; HttpOnly
3. Verify Origin and Referer Headers
Check the Origin or Referer header on incoming requests. If they don't match your expected domain, reject the request. This is a simple but effective additional layer.
4. Use Custom Headers for AJAX
If your API is consumed via JavaScript, require a custom header like X-Requested-With. Browsers enforce CORS preflight for custom headers, so a simple form POST from a malicious site won't include it.
Configuring CORS Properly
CORS misconfigurations can also lead to security issues. The most common mistake is setting Access-Control-Allow-Origin: * while also allowing credentials. That combination is forbidden by browsers, but developers sometimes try to work around it insecurely.
Best Practices for CORS Headers
- Specify exact origins instead of wildcards when credentials are involved.
- Limit allowed methods to only what your API supports (e.g.,
GET, POST). - Restrict allowed headers to those your app actually uses.
- Set
Access-Control-Max-Ageto cache preflight responses and reduce overhead. - Avoid reflecting the
Originheader blindly; validate it against a whitelist.
# Example: Nginx configuration for CORS
location /api/ {
if ($http_origin ~* (https://(app|admin)\.example\.com)) {
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
}
if ($request_method = 'OPTIONS') {
return 204;
}
}
Remember that CORS is enforced by the browser, not the server. It's not a substitute for authentication or authorization; it's a way to relax the same-origin policy safely.
Putting It All Together
Securing browser requests requires a layered approach. Here's a quick checklist:
- Use anti-CSRF tokens for all state-changing operations.
- Set SameSite cookies to
LaxorStrict. - Validate Origin/Referer headers on the server.
- Configure CORS precisely: whitelist origins, limit methods, and avoid wildcards with credentials.
- Regularly test your app with security scanners and manual checks.
By understanding the distinct roles of CSRF and CORS, you can avoid common pitfalls and build a more secure web application.
FAQ
Can CORS prevent CSRF attacks?
No, CORS does not prevent CSRF. CSRF attacks do not rely on reading responses; they simply trigger requests. CORS controls which origins can read responses, but it doesn't block the request from being sent. To prevent CSRF, you need tokens, SameSite cookies, or origin validation.
Is it safe to set Access-Control-Allow-Origin to '*'?
Setting it to '*' is safe only if your API does not use credentials (cookies, HTTP authentication). If credentials are involved, browsers will reject the response. For authenticated APIs, always specify exact origins.
Do I need CSRF protection if I use JWT in Authorization headers?
If you store JWTs in cookies, you still need CSRF protection because cookies are automatically sent. If you store JWTs in memory and send them via the Authorization header, CSRF is not a concern because the attacker cannot set custom headers cross-origin. However, you must protect against XSS to keep the token safe.
Ready to test your API's CORS headers? Use our Nginx Log Analyzer to inspect request patterns and spot suspicious cross-origin attempts.