Sitemap

How to Prevent CSRF Attacks in PHP

3 min readMay 18, 2026

--

Press enter or click to view image in full size
Image created with AI by author

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 /api endpoint 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 randomness
  • bin2hex() — 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

--

--