Cross-site request forgery (CSRF) may cause users to do unintended actions on vulnerable websites. This attack relies on sending a request to vulnerable website, invoked by malicious website.
In order for a cross-site request forgery attack to able to be performed, there should be a relevant action (for example transferring the funds away to an attacker) and there should be no unpredictable parameters. Users being authenticated increase the impact of cross-site request forgery.
WARNING: We’re not responsible for damage caused by cross-site request forgery! Malicious hacking is a computer crime and you may face legal consequences! This post is meant to gain awareness about cross-site request forgery and give a way to prevent those vulnerabilities.
The impact of cross-site request forgery
Cross-site request forgery may cause unintentional actions by the victim, such as changing the email address, changing the password or transferring funds to an attacker. The attacker may even take control over the victim's account.
Example: bank transfer vulnerable to CSRF (GET method)
The user would typically send a GET request for a URL in order to transfer money: http://bank.example/transfer?dest=User2&amount=50
. The request will then transfer 50¤ to another user
But what if an attacker constructed http://bank.example/transfer?dest=H4Xx0r&amount=50000
and tricked the user into clicking the URL? The user will then lose 50000¤ by transferring it to the attacker.
What if an attacker constructed a webpage at http://mal.example/
with this HTML code:
<img src="http://bank.example/transfer?dest=H4Xx0r&amount=50000" alt="something">
If the user goes to http://mal.example/
, then the user will lose 50000¤ by transferring it to the attacker.
Example: comment section vulnerable to CSRF (POST method)
This is the example code (from the post about cross-site scripting attacks) vulnerable to CSRF attacks:
<?php
// Don't throw errors by default
$mysqli_driver = new mysqli_driver();
$mysqli_driver->report_mode = MYSQLI_REPORT_OFF;
// Message
$message = "";
// Connect to MySQL
$mysqli = new mysqli("localhost", "username", "password", "database");
// Check connection
if($mysqli->connect_errno){
die("ERROR: Could not connect. " . htmlspecialchars($mysqli->connect_error);
}
// Process form submission
if($_SERVER["REQUEST_METHOD"] == "POST"){
// Escape user inputs for security
$comment = $mysqli->real_escape_string($_POST['comment']);
// Insert comment into database
$sql = "INSERT INTO comments (comment) VALUES ('$comment')";
if($mysqli->query($sql) === true){
$message = "Comment added successfully.";
} else {
$message = "ERROR: Could not execute $sql. " . $mysqli->error;
}
}
// Retrieve comments from database
$sql = "SELECT * FROM comments";
$result = $mysqli->query($sql);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Some blog article</title>
</head>
<body>
<h1>Some blog article</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla fringilla a massa vel molestie. Phasellus a lorem nec arcu ultricies vehicula. Duis varius nec libero id pretium. Ut in ante tincidunt, mattis sem vitae, gravida tortor. Vestibulum consequat mi et dapibus hendrerit. Nunc in interdum dolor, at molestie sapien. Ut eget lobortis enim. Proin commodo bibendum dolor quis finibus. Mauris placerat dignissim sodales. Morbi leo lectus, dapibus et accumsan sit amet, ultricies ac nulla.</p>
<p>Mauris vel erat vel arcu pulvinar lacinia eu sit amet massa. Donec porttitor risus eget ex cursus placerat. Maecenas velit lectus, laoreet sed justo nec, tristique eleifend nulla. Donec hendrerit eros in blandit laoreet. In mattis elit quis accumsan ullamcorper. Morbi imperdiet molestie pulvinar. Nam lobortis, tortor ac pretium ullamcorper, leo eros laoreet risus, quis convallis mauris enim in eros. Integer lacinia commodo augue, eu malesuada nisl euismod at. Aliquam accumsan non ante vitae congue. Nam condimentum nisi quis blandit molestie. Vivamus dapibus aliquet nunc at ullamcorper. Nullam congue aliquam metus, dignissim hendrerit risus luctus eget. Fusce tempus purus purus, at varius felis finibus eu.</p>
<p>Vestibulum varius ut purus vel elementum. Donec imperdiet elit vitae enim ultrices sagittis. Aliquam erat volutpat. Donec eu justo in elit fringilla fermentum. Aenean venenatis consequat urna, posuere consectetur quam sagittis at. Integer at elit nec neque iaculis dignissim. Suspendisse non pharetra urna. Praesent tempus augue accumsan massa bibendum, id dictum nunc bibendum. Nam suscipit arcu ipsum, sed dignissim felis condimentum quis.</p>
<p>Nulla facilisi. Sed commodo augue magna, in faucibus erat mattis molestie. Sed varius tincidunt lectus, et porttitor est luctus sed. Proin ac mauris nibh. Nullam vel sollicitudin nibh, ac fermentum lectus. Suspendisse pharetra orci eu maximus interdum. Vestibulum malesuada, nibh quis rutrum luctus, nibh ligula dignissim metus, ac blandit odio quam sed metus. Suspendisse sit amet libero non elit pretium aliquet non vel arcu. Mauris lobortis porta urna, nec bibendum erat semper vitae. Curabitur vel ipsum sit amet ex suscipit euismod. Fusce sodales tortor ac vulputate euismod.</p>
<p>Etiam eu egestas dolor. Vestibulum placerat semper odio, interdum blandit erat venenatis eu. Nulla lorem justo, eleifend sit amet tortor et, varius imperdiet sem. Fusce sit amet nisl a enim pellentesque mattis. Fusce non aliquet justo. Etiam eget arcu feugiat, tincidunt enim vitae, blandit lorem. Proin non nibh sem. Aliquam ut commodo mauris. Maecenas congue orci vitae dolor vestibulum gravida. Sed libero est, hendrerit eu venenatis ut, eleifend nec enim. Vivamus id bibendum orci, ut commodo mi.</p>
<h2>Post a comment:</h2>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
<textarea name="comment"></textarea><br>
<input type="submit" value="Submit">
</form>
<?php
if ($message){
echo "<b>" . htmlspecialchars($message) . "</b>";
}
?>
<h2>Comments:</h2>
<?php
if($result->num_rows > 0){
while($row = $result->fetch_assoc()){
echo "<p>" . htmlspecialchars($row['comment']) . "</p>";
}
} else{
echo "No comments yet.";
}
?>
</body>
</html>
<?php
// Close connection
$mysqli->close();
?>
If you want to try it, there is a database structure in SQL, along with example comment (the database name is database, DBMS is MySQL/MariaDB):
-- phpMyAdmin SQL Dump
-- version 5.2.1
-- https://www.phpmyadmin.net/
--
-- Host: localhost
-- Generation Time: May 07, 2024 at 07:31 PM
-- Server version: 10.3.39-MariaDB-0ubuntu0.20.04.2
-- PHP Version: 7.4.3-4ubuntu2.22
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `database`
--
-- --------------------------------------------------------
--
-- Table structure for table `comments`
--
CREATE TABLE `comments` (
`id` int(11) NOT NULL,
`comment` varchar(5000) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
--
-- Dumping data for table `comments`
--
INSERT INTO `comments` (`id`, `comment`) VALUES
(1, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet dui dignissim orci consequat, nec accumsan sapien euismod. Quisque semper feugiat maximus. Nam efficitur imperdiet risus, in lacinia dolor efficitur in. In mollis urna at nisi rhoncus mattis. Integer eget vestibulum diam, vitae ornare leo. Nullam laoreet leo eleifend rhoncus dictum. Pellentesque ultrices dapibus vulputate. Praesent ligula enim, porttitor ac nibh sed, sagittis maximus nisl. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla dolor sapien, convallis nec venenatis ut, imperdiet eget nibh. Nulla facilisi. Praesent libero neque, pellentesque eu lorem sit amet, pulvinar molestie metus. Vestibulum ipsum ipsum, interdum eu scelerisque at, consectetur a justo. Nullam gravida dolor vel nibh aliquam luctus. ');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `comments`
--
ALTER TABLE `comments`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `comments`
--
ALTER TABLE `comments`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
Let's assume the page is hosted at http://someblog.example
.
When the user submits a comment at http://someblog.example
, the comment will then be displayed in the comment section.
But what if an attacker creates a malicious website (let's assume that the page is at http://badsite.example/viewpictures.html
), that contains this:
<form action="http://someblog.example/" method="post">
<input type="hidden name="comment" value="Go to http://badsite.example free software crack">
<input type="submit" value="View pictures">
</form>
The victim may click a "View pictures" button, and a spam comment is submitted on the post.
Cross-site request forgery vulnerability prevention
You can prevent cross-site request forgery by introducing a random token (known as CSRF token) tied to an user session.
This is an example code without the CSRF vulnerablity (using a token tied to a PHP session):
<?php
// Start the session
session_start(array(
'name' => 'session',
'samesite' => 'Strict'
)) or die('ERROR: Failed to start session');
if(!isset($_SESSION['token'])){
$_SESSION['token'] = bin2hex(random_bytes(32));
}
// Don't throw errors by default
$mysqli_driver = new mysqli_driver();
$mysqli_driver->report_mode = MYSQLI_REPORT_OFF;
// Message
$message = "";
// Connect to MySQL
$mysqli = new mysqli("localhost", "username", "password", "database");
// Check connection
if($mysqli->connect_errno){
die("ERROR: Could not connect. " . htmlspecialchars($mysqli->connect_error);
}
// Process form submission
if($_SERVER["REQUEST_METHOD"] == "POST"){
// Check for CSRF token
if(isset($_POST['token']) && isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token']) {
// Escape user inputs for security
$comment = $mysqli->real_escape_string($_POST['comment']);
// Insert comment into database
$sql = "INSERT INTO comments (comment) VALUES ('$comment')";
if($mysqli->query($sql) === true){
$message = "Comment added successfully.";
} else {
$message = "ERROR: Could not execute $sql. " . $mysqli->error;
}
} else {
$message = "ERROR: CSRF attack detected."
}
}
// Retrieve comments from database
$sql = "SELECT * FROM comments";
$result = $mysqli->query($sql);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Some blog article</title>
</head>
<body>
<h1>Some blog article</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla fringilla a massa vel molestie. Phasellus a lorem nec arcu ultricies vehicula. Duis varius nec libero id pretium. Ut in ante tincidunt, mattis sem vitae, gravida tortor. Vestibulum consequat mi et dapibus hendrerit. Nunc in interdum dolor, at molestie sapien. Ut eget lobortis enim. Proin commodo bibendum dolor quis finibus. Mauris placerat dignissim sodales. Morbi leo lectus, dapibus et accumsan sit amet, ultricies ac nulla.</p>
<p>Mauris vel erat vel arcu pulvinar lacinia eu sit amet massa. Donec porttitor risus eget ex cursus placerat. Maecenas velit lectus, laoreet sed justo nec, tristique eleifend nulla. Donec hendrerit eros in blandit laoreet. In mattis elit quis accumsan ullamcorper. Morbi imperdiet molestie pulvinar. Nam lobortis, tortor ac pretium ullamcorper, leo eros laoreet risus, quis convallis mauris enim in eros. Integer lacinia commodo augue, eu malesuada nisl euismod at. Aliquam accumsan non ante vitae congue. Nam condimentum nisi quis blandit molestie. Vivamus dapibus aliquet nunc at ullamcorper. Nullam congue aliquam metus, dignissim hendrerit risus luctus eget. Fusce tempus purus purus, at varius felis finibus eu.</p>
<p>Vestibulum varius ut purus vel elementum. Donec imperdiet elit vitae enim ultrices sagittis. Aliquam erat volutpat. Donec eu justo in elit fringilla fermentum. Aenean venenatis consequat urna, posuere consectetur quam sagittis at. Integer at elit nec neque iaculis dignissim. Suspendisse non pharetra urna. Praesent tempus augue accumsan massa bibendum, id dictum nunc bibendum. Nam suscipit arcu ipsum, sed dignissim felis condimentum quis.</p>
<p>Nulla facilisi. Sed commodo augue magna, in faucibus erat mattis molestie. Sed varius tincidunt lectus, et porttitor est luctus sed. Proin ac mauris nibh. Nullam vel sollicitudin nibh, ac fermentum lectus. Suspendisse pharetra orci eu maximus interdum. Vestibulum malesuada, nibh quis rutrum luctus, nibh ligula dignissim metus, ac blandit odio quam sed metus. Suspendisse sit amet libero non elit pretium aliquet non vel arcu. Mauris lobortis porta urna, nec bibendum erat semper vitae. Curabitur vel ipsum sit amet ex suscipit euismod. Fusce sodales tortor ac vulputate euismod.</p>
<p>Etiam eu egestas dolor. Vestibulum placerat semper odio, interdum blandit erat venenatis eu. Nulla lorem justo, eleifend sit amet tortor et, varius imperdiet sem. Fusce sit amet nisl a enim pellentesque mattis. Fusce non aliquet justo. Etiam eget arcu feugiat, tincidunt enim vitae, blandit lorem. Proin non nibh sem. Aliquam ut commodo mauris. Maecenas congue orci vitae dolor vestibulum gravida. Sed libero est, hendrerit eu venenatis ut, eleifend nec enim. Vivamus id bibendum orci, ut commodo mi.</p>
<h2>Post a comment:</h2>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
<input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>">
<textarea name="comment"></textarea><br>
<input type="submit" value="Submit">
</form>
<?php
if ($message){
echo "<b>" . htmlspecialchars($message) . "</b>";
}
?>
<h2>Comments:</h2>
<?php
if($result->num_rows > 0){
while($row = $result->fetch_assoc()){
echo "<p>" . htmlspecialchars($row['comment']) . "</p>";
}
} else{
echo "No comments yet.";
}
?>
</body>
</html>
<?php
// Close connection
$mysqli->close();
?>
The example shown above first creates or continues a PHP session, then generate a token tied to a session, when it is not present. If the user submits the comment, then it is submitted with the token. If token is not present or if it is invalid, the user will see an error message.
You can even invalidate the token for every action to make the application even more secure, although the user can't perform two actions from different browser windows.
The session cookie is set with SameSite
attribute set to Strict
, which means that the cookie will not be sent cross-site for any type of request. This provides additional security against cross-site request forgery.
You can also validate the referrer, although this is less secure protection method against cross-site request forgery attacks, due to possible open redirects in the site or user-submitted HTML content.
This is an example of a web application with referrer validation:
<?php
// Don't throw errors by default
$mysqli_driver = new mysqli_driver();
$mysqli_driver->report_mode = MYSQLI_REPORT_OFF;
// Message
$message = "";
// Connect to MySQL
$mysqli = new mysqli("localhost", "username", "password", "database");
// Check connection
if($mysqli->connect_errno){
die("ERROR: Could not connect. " . htmlspecialchars($mysqli->connect_error);
}
// Process form submission
if($_SERVER["REQUEST_METHOD"] == "POST"){
// Check for referrer
$base_url = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['REMOTE_ADDR']);
if(isset($_SERVER['HTTP_REFERER']) && substr($_SERVER['HTTP_REFERER'] . '/', 0, strlen($base_url) + 1) == ($base_url . '/')){
// Escape user inputs for security
$comment = $mysqli->real_escape_string($_POST['comment']);
// Insert comment into database
$sql = "INSERT INTO comments (comment) VALUES ('$comment')";
if($mysqli->query($sql) === true){
$message = "Comment added successfully.";
} else {
$message = "ERROR: Could not execute $sql. " . $mysqli->error;
}
} else {
$message = "ERROR: CSRF attack detected."
}
}
// Retrieve comments from database
$sql = "SELECT * FROM comments";
$result = $mysqli->query($sql);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Some blog article</title>
</head>
<body>
<h1>Some blog article</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla fringilla a massa vel molestie. Phasellus a lorem nec arcu ultricies vehicula. Duis varius nec libero id pretium. Ut in ante tincidunt, mattis sem vitae, gravida tortor. Vestibulum consequat mi et dapibus hendrerit. Nunc in interdum dolor, at molestie sapien. Ut eget lobortis enim. Proin commodo bibendum dolor quis finibus. Mauris placerat dignissim sodales. Morbi leo lectus, dapibus et accumsan sit amet, ultricies ac nulla.</p>
<p>Mauris vel erat vel arcu pulvinar lacinia eu sit amet massa. Donec porttitor risus eget ex cursus placerat. Maecenas velit lectus, laoreet sed justo nec, tristique eleifend nulla. Donec hendrerit eros in blandit laoreet. In mattis elit quis accumsan ullamcorper. Morbi imperdiet molestie pulvinar. Nam lobortis, tortor ac pretium ullamcorper, leo eros laoreet risus, quis convallis mauris enim in eros. Integer lacinia commodo augue, eu malesuada nisl euismod at. Aliquam accumsan non ante vitae congue. Nam condimentum nisi quis blandit molestie. Vivamus dapibus aliquet nunc at ullamcorper. Nullam congue aliquam metus, dignissim hendrerit risus luctus eget. Fusce tempus purus purus, at varius felis finibus eu.</p>
<p>Vestibulum varius ut purus vel elementum. Donec imperdiet elit vitae enim ultrices sagittis. Aliquam erat volutpat. Donec eu justo in elit fringilla fermentum. Aenean venenatis consequat urna, posuere consectetur quam sagittis at. Integer at elit nec neque iaculis dignissim. Suspendisse non pharetra urna. Praesent tempus augue accumsan massa bibendum, id dictum nunc bibendum. Nam suscipit arcu ipsum, sed dignissim felis condimentum quis.</p>
<p>Nulla facilisi. Sed commodo augue magna, in faucibus erat mattis molestie. Sed varius tincidunt lectus, et porttitor est luctus sed. Proin ac mauris nibh. Nullam vel sollicitudin nibh, ac fermentum lectus. Suspendisse pharetra orci eu maximus interdum. Vestibulum malesuada, nibh quis rutrum luctus, nibh ligula dignissim metus, ac blandit odio quam sed metus. Suspendisse sit amet libero non elit pretium aliquet non vel arcu. Mauris lobortis porta urna, nec bibendum erat semper vitae. Curabitur vel ipsum sit amet ex suscipit euismod. Fusce sodales tortor ac vulputate euismod.</p>
<p>Etiam eu egestas dolor. Vestibulum placerat semper odio, interdum blandit erat venenatis eu. Nulla lorem justo, eleifend sit amet tortor et, varius imperdiet sem. Fusce sit amet nisl a enim pellentesque mattis. Fusce non aliquet justo. Etiam eget arcu feugiat, tincidunt enim vitae, blandit lorem. Proin non nibh sem. Aliquam ut commodo mauris. Maecenas congue orci vitae dolor vestibulum gravida. Sed libero est, hendrerit eu venenatis ut, eleifend nec enim. Vivamus id bibendum orci, ut commodo mi.</p>
<h2>Post a comment:</h2>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
<textarea name="comment"></textarea><br>
<input type="submit" value="Submit">
</form>
<?php
if ($message){
echo "<b>" . htmlspecialchars($message) . "</b>";
}
?>
<h2>Comments:</h2>
<?php
if($result->num_rows > 0){
while($row = $result->fetch_assoc()){
echo "<p>" . htmlspecialchars($row['comment']) . "</p>";
}
} else{
echo "No comments yet.";
}
?>
</body>
</html>
<?php
// Close connection
$mysqli->close();
?>
The example shown above checks, if the referrer is the same as site address (basically if the users submits the comment from the site with post and not by malicious site). If the user submits the comment from different site than the site address (with a post), then the user will see an error message.
Conclusion
In conclusion, cross-site request forgery (CSRF) poses a significant threat to the security of web applications, potentially leading to unintended actions by users, such as transferring funds. This attack relies on exploiting the trust between a user's browser and a vulnerable website, often through the manipulation of HTTP requests.
To mitigate the risks associated with CSRF attacks, it's essential for developers to implement robust security measures. Introducing random tokens, known as CSRF tokens, tied to user sessions can effectively prevent CSRF attacks by ensuring that each request originates from a legitimate source. Additionally, validating the referrer can provide an extra layer of security, although it's less reliable due to potential open redirects or user-submitted HTML content.
By adopting these preventive measures and staying vigilant against emerging threats, developers can help safeguard web applications and protect users from the detrimental effects of CSRF attacks. Remember, prioritizing security not only enhances user trust but also mitigates the risk of legal consequences associated with malicious hacking activities.