CORS, or Cross-Origin Resource Sharing, is essentially a server’s guest list. By default, browsers block websites from reading data from another server. CORS is the mechanism a server uses to tell the browser, "It's safe to let this specific website access my responses."
CSRF, or Cross-Site Request Forgery, is an attack that tricks your browser into performing an unwanted action using your existing login. CSRF protection helps the application reject that action. MDN on CORS, MDN on CSRF
Let's follow one example: saving an article on our blog, Build Failed.
Saving an article normally
You sign in at https://buildfailed.dev/auth. The server gives your browser a session cookie, which identifies your login on eligible later requests.
When you click Save, the dashboard sends the article to the API. The browser includes the cookie, the server recognizes your account, and it checks whether you can save articles.
The cookie answers “Which account is making this request?” But recognizing an account does not, by itself, establish that the request came through the expected dashboard flow.
How another website could interfere
Imagine a version of our blog that accepts article saves from ordinary HTML forms and has no CSRF protection.
You are logged in to our blog. In another tab, you open a different website and click a button labeled “Show the picture.”
But the button does something else: it sends our blog a request saying “Save this article.” The article's text was written by the attacker.
If your browser includes your login cookie, the blog recognizes your account. Without CSRF protection, it could accept the request and save the attacker's article under your account.
The attacker did not steal your login. They made your browser use it for an action you did not intend. That is CSRF.
This example requires the server to accept the form submission. A normal HTML form cannot directly submit JSON to a strictly JSON-only endpoint. MDN on how CSRF works
What a CSRF token adds
Think of CSRF protection as an extra check alongside authentication. Authentication identifies your account; CSRF protection requires something beyond the automatically sent login cookie. It is not two-factor authentication, because it does not ask you to prove your identity again.
A CSRF token is an unpredictable secret shared between the legitimate application and the server. In a session-based setup, it belongs to that session, rather than being one common key for every user. The application submits it with protected actions:
Cookie: SESSION=example-session-id
X-CSRF-TOKEN: example-unpredictable-token
The server validates the token before saving. The attacker's form cannot ordinarily supply it, and untrusted websites must be unable to read the response that exposes it. Use the framework's implementation for generating and validating tokens. OWASP on CSRF defenses
With a JWT (JSON Web Token), what matters is how it is sent. A JWT in an automatically sent cookie can still need CSRF protection. If an API accepts only JWTs explicitly added to the Authorization header, without automatic authentication cookies, the usual cookie-based CSRF attack generally does not apply. Spring Security on stateless authentication and CSRF
Cookie settings such as SameSite can also block some attacks by restricting when cookies accompany requests from other sites. They provide another layer of protection. MDN on cookie settings
When CORS enters the picture
Now imagine moving the blog API to https://api.buildfailed.dev, while keeping the dashboard at https://buildfailed.dev/auth.
An origin has three parts: scheme + hostname + port. Here is how the browser sees these two addresses:
| Part | Dashboard | API |
|---|---|---|
| Scheme | https |
https |
| Hostname | buildfailed.dev |
api.buildfailed.dev |
| Port | 443 |
443 |
The port is not written in either URL because HTTPS uses port 443 by default. The /auth part is a path; it is not part of the origin.
Both addresses use the same scheme and port, but their hostnames differ, so they have different origins. All three parts must match for two addresses to have the same origin.
The browser restricts JavaScript from reading responses from another origin unless the server allows it. To let the dashboard read a public article list, the API can return:
Access-Control-Allow-Origin: https://buildfailed.dev
That is CORS permission. The header belongs on the API's response, and the browser enforces it. Authentication and authorization remain the server's responsibility. MDN on the same-origin policy, MDN on CORS
Why reading and saving are separate
For a cross-origin JSON save with a custom CSRF header, the browser first checks permission through an OPTIONS request, called a preflight, unless suitable permission is cached. If preflight fails, the intended save request is not sent. MDN on preflight requests
Browser API
|--- OPTIONS ---------------->|
|<-- CORS permission ---------|
|--- Save + cookie + token --->|
| | Check login, permissions, and token
|<-- Response + CORS headers --|
However, ordinary form submissions can skip preflight. An unwanted save could therefore reach an unprotected server even though the attacker's page cannot read the response. The attacker only needs the save to happen.
Blocking access to a response does not undo an action already performed. That is why a CORS configuration alone does not establish CSRF protection. MDN on requests without preflight
Summary
- CORS: tells the browser which other websites may read our API's responses. Think: “Who may read this data?”
- CSRF: an attack that tries to use your existing login for something you did not request. CSRF protection helps stop another website from saving or deleting an article through your account.
