Explaining the terms
CORS (Cross-Origin Resource Sharing) is a protocol to allow two different domains to talk to each other.
A CORS (preflight) request needs to include Origin
attribute in header. A response of a CORS preflight request contains Access-Control-Allow-Origin
attribute in header to allow/restrict access from a different domain. From MDN web docs:

The attribute Origin is in every request header except for GET
and HEAD
request. A CORS (preflight) request must contain Origin
attribute, but not every request with Origin
attribute is a CORS (preflight) request
The same-origin policy is a mechanism to restrict domains to talk to each other.
The CSRF token (Cross-Site-Request-Forgery) is stored in the session of the user and has to be send along with a POST/DELETE/PUT request. On the server side, the CSRF token will be compared to the value in the session and only allow to continue if they match.
Same origin policy prevents communication across domains
All browsers implement the same-origin policy. This policy avoids in general that a web application on domain A can make a HTTP request to an application on domain B. However, it does not restrict all requests. For example same-origin policy does not restrict embed tags like this:
<img src="https://dashboard.example.com/post-message/hello">
It’s irrelevant whether the response is a valid image — the request is still executed. This is why it’s important that state-changing endpoints on your web application cannot be invoked with the GET method.
CORS Protocol: Preflight check
You may use CORS to avoid same-origin policy and let the domain A make a request to domain B that would otherwise be forbidden.
Before the actually request is send, a preflight request will be send to check if the server allows domain A to send this request type. If it does, domain B will have Access-Control-Allow-Origin
field in the response header and domain A will sent the actual request.
For example, if no Access-Control-Allow-Origin
is set, then a Javascript XMLHttpRequests would be restricted for domain A by a preflight, without executing the request on domain B.
Why you need CSRF token despite same-origin policy
If same-origin policy would work for all types of request there would be no need to use CSRF token, because one would have full protection by the same-origin policy and could use CORS to explicitly state which domain can communicate. However, this is not the case. There are a couple of HTTP requests that do not send a CORS preflight request!
GET, HEAD and POST requests with specific headers and specific content-type do not send a preflight request. Such requests are called simple requests. This means the request will be executed and if the request was not allowed, then a not-allowed error response will be returned. But the problem is, that the simple request was executed on the server.
As forms existed before CORS and where already allowed to send stuff to any origin, one didn't want to break the existing internet with pre-flight requests for POST
requests. See also here:
The motivation is that the element from HTML 4.0 (which predates cross-site XMLHttpRequest and fetch) can submit simple requests to any origin, so anyone writing a server must already be protecting against cross-site request forgery (CSRF). Under this assumption, the server doesn't have to opt-in (by responding to a preflight request) to receive any request that looks like a form submission, since the threat of CSRF is no worse than that of form submission. However, the server still must opt-in using Access-Control-Allow-Origin to share the response with the script.
Unfortunately, a plain <form action="POST">
creates a simple requests!
And because of these simple requests, we have to protect the POST routes with CSRF tokens (GET routes don't need CSRF because they can be read anyway by embedded tags as shown above. Just make sure you don't have a state-changing get method).
Is checking origin of request enough?
Since the POST request is sending the origin along in the header and browser disallow modifying it, one could wonder if checking origin alone is enough, without the pre-flight check. However, scripts that are executed on CLI can set origin to any arbitrary value. Also, it still may be possible to change the origin due to a combination of plugins that the user may have available. Another thing is that browsers may fail to support it (Edge was released in April 2015, but supported origin header in June 2015, in Firefox was a bug that was fixed in 2008 https://bugzilla.mozilla.org/show_bug.cgi?id=446344).
From wikipedia:
Various other techniques have been used or proposed for CSRF
prevention historically. Verifying that the request's headers contain
[...] HTTP Origin header. However, this is insecure – a combination
of browser plugins and redirects can allow an attacker to provide
custom HTTP headers on a request to any website, hence allowing a
forged request
Thus its recommended to use a CSRF token instead of relying on origin header. But there are cases, where it may be better to not have a CSRF token, because of session expiration.
Problem with CSRF tokens
I would avoid to use CSRF tokens for contact forms. As it may lead to a situation, that a user wrote for a long time and when he submits, the session is outdated, CSRF token is invalid, and the whole message of the user is removed. That would be a terrible user experience.
For a contact form, you want to avoid getting messages from SPAM, so recaptcha3 may be reasonable choice, as it does not require user interaction.
You could also increase the life-time of sessions on your browser, but this may end up in a large storage size. Alternatively, you could use cookie hashed session that are stored at a client. Those session should also not expire, even if the client has to make a 12 hour break before submitting the form.
My summary
- Use CRSF token for critical endpoints, like login or shop checkout
- For practial reasons, I would suggest not to use CSRF token for application forms or contact forms.
- Never have a state-changing GET endpoint