How to Prevent CSRF Attacks in PHP
A simplified version of my PHP app submits forms this way:
<form action="/api/" method="post">
<input type="text"name="taskName" id="taskName">
<input type="hidden" name="taskAddUpdate" value="1">
<input type="hidden" name="taskId" id="taskId" value="1">
<input type="submit" value="Submit">
</form>And my PHP controller is:
<?php
if (array_key_exists('taskAddUpdate', $_POST)) {
$taskName = $_POST['taskName'] ?? '';
// ... more here
}
?>The problem is that an attacker could create the following HTML on their site:
<!-- Invisible, auto-submits on page load -->
<form method="POST" action="https://<yoursite.com>/api/" id="f">
<input name="action" value="taskAddUpdate">
<input name="taskName" value="HACKED">
</form>
<script>document.getElementById('f').submit();</script>What happens next?
- The browser visits your
/apiendpoint and automatically includes cookies (including the session) - Your server sees a valid logged-in user
- The request executes successfully
This is called a Cross Site Scripting (“XSS”) Attack, as there’s another domain that’s interacting with yours.
The Solution
The fix is simple in concept:
Every request must prove it came from your app: not just the user’s browser.
You can do this using a CSRF token:
- A random, secret value
- Stored in the session
- Sent with every legitimate request
- Verified on your server
An attacker cannot read your site’s content (due to same-origin policy), so they can’t steal the token.
How To Implement
Step 1: Generate and store the token in the session
Put this right after session_start() :
<?php
// Generate CSRF token once per session
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>Why this works
random_bytes(32)— cryptographically secure randomnessbin2hex()— safe for HTML
Step 2: Create Helper Functions
Create these PHP functions:
<?php
function csrfField() {
$token = $_SESSION['csrf_token'] ?? '';
return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($token, ENT_QUOTES, 'UTF-8') . '">';
}
function csrfVerify() {
// Accept token from POST body (form submissions) or X-CSRF-Token header (Ajax)
$submitted = $_POST['csrf_token']
?? $_SERVER['HTTP_X_CSRF_TOKEN']
?? '';
$expected = $_SESSION['csrf_token'] ?? '';
if ($submitted === '' || $expected === '' || !hash_equals($expected, $submitted)) {
http_response_code(403);
die('Invalid request (CSRF token mismatch).');
}
}
?>Step 3: Add Token To Every Form
In every HTML form, add it in:
<form method="POST" action="/api/">
<?php print csrfField(); ?> <!-- ADD THIS IN -->
<input type="hidden" name="action" value="taskAddUpdate">
</form>Now every legitimate request carries the token.
Step 4: Verify Every POST Request
At the top of the controller’s code block, add this in
<?php
if (array_key_exists('taskAddUpdate', $_POST)) {
csrfVerify(); // << NEW
$taskName = $_POST['taskName'] ?? '';
// ... more here
}
?>Better yet, put in a centralized place; such that it doesn’t have to be in every code block. You could do something like this at the top of the controller:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrfVerify();
}
?>This guarantees:
- No endpoint is forgotten
- Protection is consistent
Step 5: Handle Ajax Requests
Add this to your meta tag:
<meta name="csrf-token" content="<?php echo htmlspecialchars($_SESSION['csrf_token'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">Add this to your JavaScript:
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
$.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRF-Token', csrfToken);
}
});And add this to the PHP:
<?php
function csrfVerify() {
$submitted = $_POST['csrf_token']
?? $_SERVER['HTTP_X_CSRF_TOKEN']
?? '';
$expected = $_SESSION['csrf_token'] ?? '';
if ($submitted === '' || $expected === '' || !hash_equals($expected, $submitted)) {
http_response_code(403);
die('Invalid request (CSRF token mismatch).');
}
}
?>And call that function in the PHP controller.
The Result:
- ✅Legitimate Form: protected via hidden input
- ✅Ajax Request: protected via Header
- ❌ Malicious Site: no access to a token, gets a 403 error
The Takeaways
- Sessions alone do not prove intent
- Browsers automatically send cookies — attackers rely on this
- CSRF tokens add proof of origin
