If you read this article, you must have known these abbreviations and have some basic understanding of web security. This article will briefly guide you to existing web security standards, the most common web attacks, and prevention methods.
SOP (Same Origin Policy) — is an important concept in the web application security model. The source combines the scheme (protocol), hostname, and port.
Websites that use the same scheme, hostname, and port have the same origin and are otherwise the сross-origin. For instance, http://site.store.com and http://api.store.com are the same sites but have various sources.
The policy rule suggests that if both websites have the same source, then scripts on one website can only get data from another.
For example, on http://site.store.com, we want to show content from http://api.store.com. A GET request returns an error:
If you look closer at the details of this request, you can notice one thing. The browser showing the request status as a CORS error:
However, if you open the details, the request was successful and received the data according to the Content-Length. However, the browser will not show the response.
CORS (Cross-Origin Resource Sharing) is a mechanism that allows requesting restricted resources from outside the domain of a web page.
CORS requests can be divided into simple and preflighted. Simple requests are considered “safe” (“Request methods are considered “safe” if their defined semantics are essentially read-only” from RFC HTTP/1.1). Requests that won't change data on the server.
CORS list of safe methods and headers:
GET and HEAD are methods for reading data. POST is allowed because such requests were already widely used before the creation of CORS (for example, POST requests that are made using HTML form) and could break many sites if they were forbidden.
According to browser policy, simple requests use methods and headers from the safe list. Browsers do not block simple requests.
Let me remind you, earlier, we made a GET request from the source http://site.store.com to the source http://api.store.com.
However, the following conclusions can already be drawn:
- It was a GET request, and the Content-type was text/plain, so we made a simple request.
- By policy, the browser doesn’t block simple requests, which means the request was successful.
- Since the browser did not show us the response body—the request was blocked.
CORS doesn’t block simple requests but blocks reading the response body.
Let's try to make a preflight request, such as DELETE:
We will get precisely the same CORS error as in the previous request. However, if you open the browser console, you can see two requests:
The first request is the DELETE request we sent earlier. The second request has a preflight type specified. If we look at the details of this request, we will see that this is a request with the OPTIONS method. You can also notice that the Access-Control-Request-Headers header with the DELETE method was sent in the request.
The browser sends a preflight request to understand what (non-simple/unsafe) requests the source allows. In response to a preflight request, the browser will wait for specific headers, based on which it will then decide whether to send or block the request.
The browser expects one or more of the following HTTP headers:
- Access-Control-Allow-Origin (authorized sources)
- Access-Control-Allow-Methods (allowed methods)
- Access-Control-Allow-Headers (allowed headers)
- Access-Control-Allow-Credentials (true/false, do need to pass cookies).
If we parse the case with a DELETE request, then send only the OPTIONS request, and since the browser did not receive any Access-Control-Allow headers in the response, it simply blocked the DELETE request. If we open the details of this request, we will see that it was indeed not sent:
Complex requests CORS first checks and then decides to block or allow them.
How to set up CORS for simple requests
Let's set up our first simple request (also set up for any other simple request) to be able to read the response body. It will be enough to send the Access-Control-Allow-Origin header, which http://api.store.com should send to http://site.store.com, allowing this origin to receive/read its data.
The same header in the response:
You can only specify one source in this header. Or you can specify the value "*", but it will allow requests from any source.
A list of authorized sources is usually maintained when multiple sources are needed. If the given source (from the Origin header) is in the list, it is written to the Access-Control-Allow-Origin header.
How to set up CORS for preflight requests
First, we allow a complex DELETE request (the configuration of any other preflight request is the same). Next, in response, you will need to send two headers, Access-Control-Allow-Origin and Access-Control-Allow-Methods.
You can, in the Access-Control-Allow-Methods header, specify multiple methods separated by commas:
Specifying the “safe” GET / HEAD / POST methods in the header is also unnecessary. CORS will not block these methods even if they are not in the Access-Control-Allow-Methods header.
These headers must be sent in both the OPTIONS response and the DELETE request.
If you send headers in the response only to the OPTIONS and not send it to the DELETE request, then the DELETE request will pass, but the browser will show a CORS error. Therefore, it will not be possible to read the response, as was the case with the first GET request.
If you send a request with a method that the source did not allow, you will get an error that this method is not in the Access-Control-Allow-Methods header:
If you send an "insecure" Content-type header, the browser will send the Access-Control-Request-Headers header in the request and wait for this header in the Access-Control-Allow-Headers in response.
By default, no cookies are sent in complex cross-origin requests. Therefore, tools such as XMLHttpRequest and Fetch need to be configured:
- XMLHttpRequest: xhr.withCredentials = true;
- Fetch: credentials: ‘include’
The source that accepts the request must confirm cookies can be used. To do this, the source sends an Access-Control-Allow-Credentials header whose value is "true." But then Access-Control-Allow-Origin must be specified. Otherwise, we get an error:
If the value "*" were allowed, it would mean that any origin can make a request using cookies.
If there is no Access-Control-Allow-Credentials header, then you can get an error:
In summary, you can customize the CORS policy for the origin with these four additional headers.
CSRF (Cross-Site Request Forgery) is a web attack that exploits loopholes in the SOP policy, and CORS does not block them. The attack consists of the attacker running malicious scripts in the victim's browser, and thus the victim performs unintended actions on his behalf.
CSRF attack will work if application authorization is based on cookies. The attack uses the knowledge that if the user has cookies for the domain, the browser will automatically add them to the request.
How can a CSRF exploit be triggered
The attacker places a malicious script on his or her website. Then encourages victims to follow the link. If the victim is logged in to the attacked site, the browser will automatically add session cookies to the request. The attacked site will process the request, treating it as made by the victim user. Some exploits can use the GET method and be completely self-contained, so the attacker does not need to use an external site.
Imagine that a user has logged in to http://bank.com and created a session cookie.
The attacker posted the following exploit on his or her website:
When the user goes to the attacker's site, the invisible HTML form will automatically work and send a POST request to change the mail.
You can see that the browser has automatically added session cookies:
This is a simple request (POST with Content-type application/x-www-form-urlencoded because made it via an HTML form), so CORS didn't block it.
Ways to protect against CSRF attacks
Content-Type based protection
Usually, POST requests that do some kind of sensitive action do not accept application/x-www-form-urlencoded, as in the example above. And then you might think that if the handlers in the application accept only application/json, this can protect against CSRF attacks because CORS will block such a request.
But sometimes, you can bypass this protection. You can change the Content-type to text/plain and simulate the desired format:
Additional characters are added to the form data format to make it a valid JSON format.
The attacker changes the exploit:
If the backend does not validate the Content-type, then this can work.
For example, we wrote golang handlers using the standard encoding/json package. We also used the echo and gin frameworks for the experiment. In echo, the (context).Bind method returned status code 415 Unsupported Media Type, and we used the (context) in gin.BindJSON method, the method did not return an error and skipped the request.
Some developers use unusual Content-types to protect against CSRF attacks. Such protection is called content-type based protection. We don’t recommend relying only on this type of protection method because it can imitate data format, and there may not be a Content-type check on the backend.
Used to configure cookie sending for cross-domain requests. Added to the Set-Cookie header: Set-Cookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Strict
It can have two meanings:
- Strict: the browser will not include cookies in any cross-origin requests;
- Lax: the browser will only include the cookie on GET cross-origin requests.
In Google Chrome, one feature related to cookies can be noticed: cross-origin requests except GET no longer receive cookies two minutes after creation.
In the picture, the request is made from http://evilsite.com (Origin header).
But for example, in Firefox, you can get a cookie after 1 hour or more:
Since February 2020, Google has implemented a new cookie security model. Very few developers follow the recommended practice of specifying the SameSite attribute when creating a cookie. This leaves many cookies and exposes your sites to possible CSRF attacks. And to increase security, the Chrome browser decided to set the value of the SameSite attribute by default equal to Lax, if the value is not specified at creation.
After the implementation of this model, many sites stopped working correctly. And Google then made a temporary feature, POST + Lax, when cookies are transmitted in POST requests within 2 minutes after they are created.
After introducing the new model, other browsers also announced plans to implement it in their browsers. For example, when creating cookies in Firefox, if SameSite is not specified, a warning is shown that cookies will soon be considered Lax by default.
If you want cookies to work in the old way, you need to specify SameSite equal to None when creating.
Can SameSite protect against CSRF attacks?
SameSite in Lax mode provides partial protection because the POST method is used more in attacks. But you should check if the web application does not recognize HTTP methods. In this situation, even if the application uses the POST method by design, it may accept requests that are switched to using the GET method. It is also possible that the application uses GET requests to perform sensitive actions. For these reasons, relying solely on the SameSite attribute is not recommended.
CSRF token is a value generated on the server side and passed to the client. The client then includes the token in all subsequent HTTP requests.
There are several patterns for using CSRF tokens.
- Synchronizer Tokens are when a token is generated and stored on the server along with the user session. Clients receive the token. In subsequent requests, the client passes the token in the HTTP header. The server compares the tokens.
- A Double Submit Cookie generates a token along with a CSRF cookie. The client receives the CSRF token and the Set-Cookie header. The client returns a CSRF token on subsequent requests, and the browser sends a CSRF cookie. The server compares the token from the cookie and the token from the header. By doing this, you do not need to store tokens on the server.
Using CSRF gorilla/mux middleware - https://github.com/gorilla/csrf
- Server creation:
- POST /login—system authorization.
Further endpoints protected by authorization:
- GET /user—get user profile.
- POST /update_email—mail change.
Using csrf.Protect, we create and configure CSRF middleware. The key is needed to encrypt the cookie data.
2. Passing a CSRF token to a client
When a user requests a profile (GET /user), we generate a CSRF token and pass it to the client. We get the token using the Token() method:
If we look at the request, we will see the __gorilla_csrf cookie and the custom X-Csrf-Token header (these names can be changed when setting up the CSRF middleware).
3. The client processes the response, receives the X-Csrf-Token header, and sends it on subsequent requests, for example, when changing mail:
Flowchart of CSRF middleware operation
When you receive a profile (GET /user):
- Trying to get a token from the __gorilla_csrf cookie.
- Since we authenticated for the first time, we do not have a CSRF token and cookie. We get an error when trying to decrypt the token and get the base token.
- A new base token is generated and encrypted. A __gorilla_csrf cookie is created.
- A masked token is created (so that there is a unique token for each request). The masked token is stored in the context and can be obtained via the csrf.Token() method (we call getUser in the handler).
- The HTTP method is checked, since we have GET, the token is not checked.
- We get status 200, header X-Csrf-Token and Set-Cookie __gorilla_csrf.
When changing mail (POST /update_email):
- Trying to get a token from the __gorilla_csrf cookie.
- Trying to decrypt the token and get the base token. Since we already have tokens, the action is successful.
- Creating the masked token (so there is a unique token for each request).
- Checking the HTTP method. POST requests require the token to be checked.
- Checks for HTTPS protocol. (In our example, HTTP is used). But if it were HTTPS, there would be a check for the Referrer header (from which source made the transition). If the Referrer and the source to which the request is made the match, then there will be no error. You can also specify the list of TrustedOrigins when setting up the middleware (if, for example, the request should be made from another origin).
- Decrypts the token from the X-Csrf-Token header to get the base token.
- A comparison is made between the cookie and header base tokens.
- Since they match, we get status 200. Otherwise, there would be status 403.
To sum up
We reviewed the SOP policy rules, the CORS mechanism, and how to set up a CORS policy. As you can see, the SOP policy solves many security issues, but it's quite restrictive. And CORS came about to ease policy and configure access between different origins.
We considered what CSRF attacks are and what methods of protection exist. At its core, CSRF attacks function through the trust model in browser standards. When an attacker clicks on a link, the browser initiates an HTTP request on behalf of the user, regardless of the link's source. This trust results in an authorization cookie being sent along with the request. CSRF attacks have been around for a long time, but so far, these attacks are common on the web and are easy to implement. Maybe soon, browser standards will change and make cross-site request forgery more difficult.