BOOK THIS SPACE FOR AD
ARTICLE ADThis guide aims to provide a comprehensive understanding of CSRF, how it works, and how to prevent it.
Cross-Site Request Forgery (CSRF/XSRF) is a type of attack that tricks the victim into submitting a malicious request. It occurs when a malicious website, email, blog, instant message, or program causes a user’s web browser to perform an unwanted action on a trusted site for which the user is authenticated (This mean that the user must be logged in to have a cookie which using to preform the unwanted action),
A CSRF attack works by including a link or script in a page that accesses a site to which the user is authenticated. The attacker does this by tricking the user into clicking a link that contains the malicious request.
A CSRF Allows an attacker to partly circumvent the same origin policy, which is designed to prevent different websites from interfering with each other.
It is important to note that CSRF vulnerabilities can be prevented by using anti-CSRF tokens and same site cookies:
Anti-CSRF tokens are random values associated with a user’s session and are typically embedded within forms to protect against CSRF attacks.SameSite cookies (I will discuses this) provide a way to ensure that browser requests to a site are only sent when the site is the one being visited.The impact of a CSRF attack can be significant, depending on the permissions the authenticated user has. If the user has administrative privileges, a CSRF attack could potentially compromise the entire web application. For example, the attacker could change the user’s email address, change their password, make purchases in the user’s name, or even perform actions that could lead to data loss or system shutdown. Therefore, it’s crucial to implement effective protection against CSRF attacks.
It can appear in many position, we can look for it in any request preform any add, update, delete actions for example, in add post, change any user date, transfer fund, delete user account,…,etc.
it’s like IDOR if there is no powerful protection technique against it, will appear everywhere in our application.
let’s take a look at some simplified examples of vulnerable code in different programming languages:
Python (Flask)Consider a Flask application that handles fund transfers:
@app.route('/transfer', methods=['POST'])def transfer():
amount = request.form['amount']
to_account = request.form['to_account']
# Transfer the amount
# ...
This code can be vulnerable to CSRF because it doesn’t verify the source of the request. An attacker can trick a user into submitting a form from another site that sends a POST request to this route, initiating a transfer.
JavaScript (Express.js)Here’s a similar function in an Express.js application:
app.post('/transfer', function(req, res) {var amount = req.body.amount;
var to_account = req.body.to_account;
// Transfer the amount
// ...
});
Again, this code doesn’t check where the POST request is coming from, so it’s vulnerable to CSRF attacks.
2. PHP
In PHP, you might have a script like this:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {$amount = $_POST['amount'];
$to_account = $_POST['to_account'];
// Transfer the amount
// ...
}
This PHP script is also vulnerable to CSRF because it doesn’t validate the source of the POST request.
In each of these cases, the code can be made more secure by including an anti-CSRF token in the form, and then checking that token when the form is submitted. If the token doesn’t match the one that was included in the form, the request can be rejected, preventing CSRF attacks.
There is 3 main methods (Anti-CSRF Tokens, SameSite Cookie and Referer based validation) to prevent against it, I will discuses them and show some code examples.
Anti-CSRF tokens are random values associated with a user’s session. They are typically included in forms or added to AJAX requests to protect against CSRF attacks. The server checks these tokens before processing the request to make sure that it originated from the correct, trusted source.
How are they generated?
The tokens are usually generated using a secure random number generator when the user’s session is created. The server maintains a record of this token.
How are they checked?
When a form is submitted (or an AJAX request is made), the server checks the submitted token against the stored value. If they match, the request is allowed, otherwise, it is rejected.
Let’s have a look at some examples:
Python (Flask):
from flask import Flask, session, requestfrom flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
csrf = CSRFProtect(app)
@app.route('/transfer', methods=['POST'])
@csrf.exempt
def transfer():
token = session.get('csrf_token', None)
if not token or token != request.form.get('csrf_token'):
abort(403)
amount = request.form['amount']
to_account = request.form['to_account']
# Transfer the amount
# ...
In this code, the Flask-WTF extension is used to generate a CSRF token when the session is created. This token is then embedded in the form. When the form is submitted, the ‘transfer’ route checks the token in the form against the one in the session.
JavaScript (Express.js with csurf middleware):
var express = require('express')var cookieParser = require('cookie-parser')
var csrf = require('csurf')
var bodyParser = require('body-parser')
var app = express()
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(csrf({ cookie: true }))
app.post('/transfer', function(req, res) {
if (req.body._csrf !== req.csrfToken()) {
return res.status(403).send('Invalid CSRF token')
}
var amount = req.body.amount;
var to_account = req.body.to_account;
// Transfer the amount
// ...
});
In this Express.js example, the ‘csurf’ middleware is used to generate a CSRF token and add it to the session. The token is included in the form as a hidden field. When the form is submitted, the ‘transfer’ route compares the token in the request with the one in the session.
PHP:
session_start();if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('Invalid CSRF token');
}
$amount = $_POST['amount'];
$to_account = $_POST['to_account'];
// Transfer the amount
// ...
} else {
// Generate a token for the session
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
In this PHP example, a CSRF token is generated when the session starts and stored in the session. It is included in the form as a hidden field. When the form is submitted, the script checks the token in the request against the one in the session.
SameSite Cookies are a property of cookies that can be set to control their behavior. The SameSite attribute can have one of three values: Strict, Lax, or None.
Strict: The cookie will only be sent in a first-party context, i.e., it will not be sent with requests initiated from third-party websites. This can help prevent CSRF attacks but can also disrupt the user experience because the cookie won’t be sent if the user follows a link from another site to your site.Lax: The cookie is withheld with cross-site subrequests (like loading images), but is sent when a user navigates to the URL from an external site (like following a link). This is a balanced approach, providing CSRF protection while maintaining a good user experience.None: The cookie will be sent in all requests, regardless of whether they are same-site or cross-site. This setting should be used with caution, as it doesn’t protect against CSRF attacks.The SameSite attribute is set when the cookie is created. Below are examples of how to set the SameSite attribute in different languages.
Flask (Python):
Flask lets you set the SameSite attribute when setting a cookie:
from flask import Flask, make_responseapp = Flask(__name__)
@app.route('/')
def index():
response = make_response("Hello, World!")
response.set_cookie('my_cookie', 'cookie_value', samesite='Lax')
return response
In this code, the set_cookie method is used to create a cookie with the SameSite attribute set to 'Lax'.
Express.js (JavaScript):
In Express.js, the cookie function of the response object is used to set cookies. The SameSite attribute can be included in the options:
var express = require('express')var app = express()
app.get('/', function (req, res) {
res.cookie('my_cookie', 'cookie_value', { sameSite: 'lax' });
res.send('Hello World');
})
app.listen(3000)
This code creates a cookie named ‘my_cookie’ with the SameSite attribute set to ‘lax’.
PHP:
In PHP, the setcookie function is used to create cookies. The SameSite attribute can be included in the options:
<?phpsetcookie('my_cookie', 'cookie_value', [
'samesite' => 'Lax',
]);
?>
In this code, the setcookie function is used to create a cookie named 'my_cookie' with the SameSite attribute set to 'Lax'.
By setting the SameSite attribute, you can control when the cookie is sent, helping to prevent CSRF attacks by ensuring that the cookie is not sent with requests initiated from third-party websites.
Referer-based validation is another technique used to protect against CSRF attacks. The idea is to check the HTTP referer header of the incoming requests and compare it to the target site’s domain. If they don’t match, the request is considered to be a CSRF attempt and is therefore rejected. This method can be effective but also has some limitations. For instance, it relies on the presence and accuracy of the HTTP referer header, which might be manipulated or blocked by certain browser settings, proxies, or firewalls.
Here are some examples of how to implement referer-based validation in different programming languages:
Python (Flask):
from flask import Flask, request, abortapp = Flask(__name__)
@app.route('/transfer', methods=['POST'])
def transfer():
referer = request.headers.get('Referer')
if not referer or 'yoursite.com' not in referer:
abort(403)
amount = request.form['amount']
to_account = request.form['to_account']
# Transfer the amount
# ...
In this Flask application, the HTTP Referer header is checked before processing the transfer. If the Referer is not from ‘yoursite.com’, the request is rejected.
JavaScript (Express.js):
var express = require('express')var app = express()
app.post('/transfer', function(req, res) {
var referer = req.headers.referer;
if (!referer || !referer.includes('yoursite.com')) {
return res.status(403).send('Invalid Referer')
}
var amount = req.body.amount;
var to_account = req.body.to_account;
// Transfer the amount
// ...
});
In this Express.js application, the ‘Referer’ in the request headers is checked. If the Referer is not from ‘yoursite.com’, the request is rejected.
PHP:
<?phpif ($_SERVER['REQUEST_METHOD'] === 'POST') {
$referer = $_SERVER['HTTP_REFERER'];
if (!isset($referer) || strpos($referer, 'yoursite.com') === false) {
die('Invalid Referer');
}
$amount = $_POST['amount'];
$to_account = $_POST['to_account'];
// Transfer the amount
// ...
}
?>
In this PHP script, the HTTP Referer header is checked before processing the POST request. If the Referer is not from ‘yoursite.com’, the request is rejected.
Remember, while referer-based validation can add an extra layer of security, it should not be the only method used to protect against CSRF attacks. It’s best utilized in combination with other methods, like anti-CSRF tokens or SameSite cookies, to provide a more robust defense.
I will use portswigger.net labs as a reference to this bypassing techniques then, we can add more from some different recourses.
NOTE :- some of the provided code examples will be different to the actual code of the labs, I just try to explain the programming concept behind the scenario.
I. Validation of CSRF token depends on request method
In some cases, the validation of the CSRF token might depend on the request method being used. For example, some applications might only validate the CSRF token for POST requests, but not for GET requests. This could be exploited by an attacker who could craft a GET request that performs the same action as a POST request, thus bypassing the CSRF token validation.
Here are some simplified examples:
Python (Flask):
@app.route('/transfer', methods=['GET', 'POST'])def transfer():
if request.method == 'POST':
token = session.get('csrf_token', None)
if not token or token != request.form.get('csrf_token'):
abort(403)
amount = request.form['amount']
to_account = request.form['to_account']
# Transfer the amount
# ...
In this code, the CSRF token check is only performed for POST requests. A GET request to the ‘/transfer’ route will bypass the CSRF token check.
JavaScript (Express.js):
app.get('/transfer', function(req, res) {var amount = req.query.amount;
var to_account = req.query.to_account;
// Transfer the amount
// ...
});
In this Express.js example, the ‘/transfer’ route accepts GET requests, which are not subject to CSRF token validation.
PHP:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('Invalid CSRF token');
}
$amount = $_POST['amount'];
$to_account = $_POST['to_account'];
// Transfer the amount
// ...
} else {
$amount = $_GET['amount'];
$to_account = $_GET['to_account'];
// Transfer the amount
// ...
}
In this PHP script, the CSRF token is only checked for POST requests. A GET request will bypass this check.
To prevent this kind of vulnerability, it’s advisable to ensure that all request methods that can change the state of the application are subject to CSRF token validation. Also, it’s generally good practice to use POST requests rather than GET requests for any operation that causes changes, as GET requests can be more easily forged.
Nevertheless, relying solely on the request method for CSRF token validation is not a robust prevention technique. An attacker may still be able to exploit a CSRF vulnerability if there are other security weaknesses in the application, such as session fixation or cross-site scripting vulnerabilities. Therefore, it’s important to implement a combination of CSRF prevention methods, including the use of anti-CSRF tokens, SameSite cookies and referer-based validation.
II. CSRF token is not tied to the user session
In scenarios where the CSRF token is not tied to the user session, the application does not validate whether the token belongs to the same session as the user making the request. Instead, the application maintains a global pool of tokens that it has issued and accepts any token that appears in this pool. This approach essentially treats CSRF tokens as globally valid, regardless of the user’s session.
This technique is vulnerable because it allows an attacker to obtain a valid CSRF token from their own session and then use it to perform actions on behalf of a victim user. Here’s how the attack works:
The attacker logs in to the application and obtains a valid CSRF token associated with their session.The attacker crafts a malicious website or email containing a request to perform an action on the application (e.g., change the victim’s email address) and includes the valid CSRF token obtained in step 1.When the victim visits the malicious website or clicks on the malicious link, their browser automatically sends the request to the application, including the valid CSRF token. Since the application does not validate the token’s association with the user’s session, it accepts the request and performs the action on behalf of the victim user.Now, let’s provide code examples in different programming languages to illustrate this vulnerability:
from flask import Flask, request, jsonifyapp = Flask(__name__)
# Global pool of CSRF tokens
tokens = set()
@app.route('/update-email', methods=['POST'])
def update_email():
global tokens
user_id = request.form.get('user_id')
new_email = request.form.get('new_email')
csrf_token = request.form.get('csrf_token')
# Check if the CSRF token is in the global pool
if csrf_token in tokens:
# Update the user's email (no further validation)
return jsonify({'message': 'Email updated successfully'})
else:
return jsonify({'error': 'Invalid CSRF token'}), 403
if __name__ == '__main__':
app.run(debug=True)const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
// Global pool of CSRF tokens
let tokens = new Set();
app.use(bodyParser.urlencoded({ extended: true }));
app.post('/update-email', (req, res) => {
const { user_id, new_email, csrf_token } = req.body;
// Check if the CSRF token is in the global pool
if (tokens.has(csrf_token)) {
// Update the user's email (no further validation)
res.json({ message: 'Email updated successfully' });
} else {
res.status(403).json({ error: 'Invalid CSRF token' });
}
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
In both examples, the server accepts any CSRF token that appears in the global pool, regardless of the user’s session. This makes the application vulnerable to CSRF attacks because an attacker can obtain a valid token and use it to perform actions on behalf of other users.
BUT, in our lab the CSRF token is using just one time and changes with each request, you must intercept one then drop the request to use the token in your exploit.
III. CSRF token is simply duplicated in a cookie
In this variation of the CSRF vulnerability, the CSRF token is simply duplicated in both a cookie and a request parameter. When the server receives a request, it verifies that the token submitted in the request parameter matches the value submitted in the cookie. This approach is sometimes referred to as the “double submit” defense against CSRF.
Here’s how this vulnerability can be exploited:
Token Duplication: The server sends the CSRF token both in a cookie and as a request parameter.Attack Process:The attacker identifies a behavior within the application that allows them to set a cookie in the victim’s browser, such as through a Cross-Site Scripting (XSS) vulnerability or by other means.The attacker leverages this cookie-setting behavior to place their cookie containing a fake CSRF token into the victim’s browser.When the victim performs an action on the vulnerable website (e.g., changing their email address), the attacker’s fake CSRF token from the victim’s browser is sent along with the request. Since the server only checks for the presence of the same token in both the request parameter and the cookie, it considers the request legitimate.3. Exploitation Difficulty: This technique is relatively simple to exploit because the attacker doesn’t need to obtain a valid token. They can simply invent a token (or guess one in the required format if validation is implemented), leverage cookie-setting behavior, and execute the CSRF attack.
Now, let’s provide some code examples using different programming languages to illustrate how this vulnerability can be implemented:
Python Flask Example:
from flask import Flask, request, make_responseimport uuid
app = Flask(__name__)
# Generate a random CSRF token
csrf_token = str(uuid.uuid4())
@app.route('/email/change', methods=['POST'])
def change_email():
csrf_from_request = request.form.get('csrf')
csrf_from_cookie = request.cookies.get('csrf')
if csrf_from_request == csrf_from_cookie:
# Process email change
return "Email changed successfully"
else:
return "CSRF token validation failed"
if __name__ == '__main__':
app.run()
JavaScript (Node.js) Example:
const express = require('express');const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const uuid = require('uuid');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
// Generate a random CSRF token
const csrfToken = uuid.v4();
app.post('/email/change', (req, res) => {
const csrfFromRequest = req.body.csrf;
const csrfFromCookie = req.cookies.csrf;
if (csrfFromRequest === csrfFromCookie) {
// Process email change
res.send('Email changed successfully');
} else {
res.send('CSRF token validation failed');
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
In these examples, the CSRF token is duplicated in both the cookie and the request parameter. However, this approach is vulnerable because an attacker can easily exploit it by manipulating the cookie in the victim’s browser, thereby bypassing the CSRF protection mechanism.
I. Validation of Referer depends on header being present
Understanding the Vulnerability: Some web applications rely on the Referer header for security checks. If the Referer header is present, they validate it to ensure that the request is coming from an expected source. However, if the Referer header is absent, they skip the validation, assuming it’s a legitimate request. This opens up a vulnerability because an attacker can craft a request that omits the Referer header, thereby bypassing the validation.Exploiting the Vulnerability: One way to exploit this vulnerability is by using a META tag within an HTML page. This META tag instructs the browser to not send the Referer header when making requests from that page. Here’s an example:<!DOCTYPE html><html>
<head>
<meta name="referrer" content="never">
</head>
<body>
<!-- Malicious content here -->
</body>
</html>
Backend Handling Logic: Now, let’s consider how the backend might handle the logic behind this vulnerability.
Backend Handling Logic (Python Flask):pythonCopy codefrom flask import Flask, request
app = Flask(__name__)
@app.route('/transfer', methods=['POST'])
def transfer_money():
referer = request.headers.get('Referer')
if not referer or referer == '<https://example.com>':
# Transfer money or handle other authorized actions
return "Money transferred successfully"
else:
return "Unauthorized request"
if __name__ == '__main__':
app.run(debug=True)
In this Python Flask application, the /transfer route handles POST requests. It retrieves the Referer header from the request and checks if it's either missing (not referer) or matches the expected value (referer == '<https://example.com>'). If the Referer is missing or matches, the request is processed; otherwise, it returns an "Unauthorized request" message.
Backend Handling Logic (Node.js with Express):javascriptCopy codeconst express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.post('/transfer', (req, res) => {
const referer = req.headers.referer;
if (!referer || referer === '<https://example.com>') {
// Transfer money or handle other authorized actions
res.send("Money transferred successfully");
} else {
res.status(401).send("Unauthorized request");
}
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
In this Node.js application using Express, the /transfer route handles POST requests. It checks if the Referer header is either missing (!referer) or matches the expected value (referer === '<https://example.com>'). If the Referer is missing or matches, the request is processed; otherwise, it returns a 401 Unauthorized response.
These examples demonstrate how the vulnerability can be exploited using HTML and JavaScript and how the backend logic can be adjusted to handle requests without the Referer header appropriately in Python Flask and Node.js with Express.