# CSRF Protection in PHP Applications

> Implement CSRF protection in PHP: synchronizer token, checking Origin and Sec-Fetch-Site, SameSite cookies and login CSRF. With code example and checklist.

Source: https://phpsos.de/en/blog/csrf-schutz-php/  
As of: 25/10/2026  
Provider: PHP SOS, PHP emergency help and server security. Immediate help via WhatsApp: https://wa.me/491736494914?text=Hello%20PHP%20SOS%2C%20we%20need%20help%20with%20our%20PHP%20server.%20Company%3A%20%E2%80%A6%20Website%3A%20%E2%80%A6

**In short:** You prevent CSRF in PHP by requiring every request that changes something to carry a secret token bound to the session, which you verify on the server with `hash_equals()`. In addition, you check the `Origin` or `Sec-Fetch-Site` headers and set session cookies with `SameSite`. GET requests must never change data, and the login form needs protection as well.

## A Click Nobody Notices

An employee is logged in to the shop backend and opens a foreign page in a second tab. In the background, this page sends a form to the backend, for example to change an email address or create an account. The browser attaches the session cookie automatically. To the application, the request looks like any other from its own team.

This is cross-site request forgery, CSRF for short: a request that a logged-in user unknowingly sends to an application they trust. The attacker does not see the response, but does not need it either. It is enough that the action is executed.

CSRF does not make headlines like PHP2Shell, but it is a frequent finding in self-developed PHP applications. Frameworks such as Symfony and Laravel come with protection; gaps arise there mainly where it has been switched off for individual routes. Custom developments without a framework often have none at all.

## CSRF in PHP: Which Requests Must Be Protected

All requests that change a state need protection: saving, deleting, ordering, granting rights, changing settings, uploading files, logging out. Read requests need no token as long as they really only read.

The first rule follows from this: **No changes via GET.** Links such as `loeschen.php?id=…` are particularly easy for foreign pages to trigger, for example through an embedded image. State-changing actions belong in POST, PUT, PATCH or DELETE.

The second rule: **Protect the login too.** With so-called login CSRF, a victim is silently logged into an attacker's account. If they enter payment data or documents there, these end up with the attacker. A token in the login form and a new session ID after a successful login prevent this.

## The Synchronizer Token Pattern in PHP

For classic applications with sessions, OWASP recommends the synchronizer token pattern: the server generates a random token, stores it in the session and expects it back in every state-changing form. It is important that the token is sent in the form or in a dedicated header, not in a cookie.

```php
<?php
declare(strict_types=1);

function csrfToken(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function csrfPruefen(): void
{
    $gesendet = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
    if (!is_string($gesendet) || !hash_equals(csrfToken(), $gesendet)) {
        http_response_code(403);
        exit('Anfrage abgelehnt.');
    }
}

if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
    csrfPruefen();
}
?>
<form method="post" action="/konto/email">
    <input type="hidden" name="csrf_token"
           value="<?= htmlspecialchars(csrfToken(), ENT_QUOTES, 'UTF-8') ?>">
    <!-- more fields -->
</form>
```

`hash_equals()` compares in constant time, so the response time reveals nothing about the correct value. After login, you should renew the session ID with `session_regenerate_id(true)` and generate a new token. Why `random_bytes()` is the right source and which functions are unsuitable is explained in [Secure Random Numbers and Tokens in PHP](https://phpsos.de/en/blog/sichere-tokens-php/).

One token per session is enough for most applications. Tokens per form or per request raise protection somewhat but lead to problems with several open tabs and the back button. More important than how often the token changes is that it never appears in URLs, where it can end up in logs, in the browser history and in referrer information sent to foreign sites.

For stateless applications without a session, OWASP names the signed double-submit cookie as an alternative: a token bound to the session by HMAC. The simple, unsigned variant is expressly not recommended.

## Second Layer: Origin, Sec-Fetch-Site and SameSite

Modern browsers send information about their origin with requests. The `Origin` header names the calling page; `Sec-Fetch-Site` says whether the request comes from the same site (`same-origin`), a related one (`same-site`) or a foreign one (`cross-site`). A simple additional check can be built from this:

```php
<?php
$erlaubteHerkunft = 'https://www.example.de';
$fetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? null;
$origin    = $_SERVER['HTTP_ORIGIN'] ?? null;

$verdaechtig = ($fetchSite !== null && $fetchSite !== 'same-origin')
    || ($fetchSite === null && $origin !== null && $origin !== $erlaubteHerkunft);

if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $verdaechtig) {
    http_response_code(403);
    exit;
}
```

OWASP stresses two points here: the origin must be compared in full so that similar-sounding domains do not slip through, and a missing header or the value `null` must not count as your own origin. Because older browsers do not always send the headers, the token remains the main defense.

The third layer is session cookies with `SameSite`. PHP supports `session.cookie_samesite` since version 7.3; the default value is empty. Set at least `Lax`; for pure backends, `Strict` is often possible. The remaining cookie settings are described in the guide [Securing Sessions in PHP](https://phpsos.de/en/ratgeber/session-sicherheit-php/).

If you use a CMS, it usually brings its own tools. WordPress, for example, works with so-called nonces, which plugins integrate through functions such as `wp_nonce_field()` and `check_admin_referer()`. In practice, gaps arise mainly in extensions that forget such checks for individual actions. So pay particular attention to self-written or little-used plugins.

For interfaces that browsers use from other domains, the CORS configuration also decides. If the server returns every requesting origin as allowed and at the same time permits cookies, it undermines the browser's protection itself. OWASP recommends opening CORS only to explicitly trusted origins; a blanket allowance of all subdomains becomes a problem as soon as one of them falls into the wrong hands.

## Checklist for Existing Applications

For grown projects, a systematic review helps:

1. Record all routes and scripts that change data, including Ajax endpoints and file uploads.
2. Check whether GET is used for them and switch to POST or suitable methods.
3. For every state-changing route, verify that a token is checked; look specifically for exceptions in the framework.
4. Test login, logout and password change separately.
5. Add `session.cookie_samesite` and the origin check.
6. Fix XSS vulnerabilities, because injected code can read any token. The approach is described in [Preventing Cross-Site Scripting in PHP](https://phpsos.de/en/ratgeber/xss-php/).

> **Important:**
> CSRF protection that is checked only in the frontend with JavaScript is ineffective. The check must take place on the server before the action is executed.

## How does PHP SOS help?

- We record all state-changing routes of your PHP application and check where a token is missing or has been switched off.
- We build in a central checkpoint instead of distributing token checks across many scripts.
- We add origin checks and SameSite settings without disrupting payment or login flows.
- After the changeover, we test again and document the results.

A complete review of your application is described by the service [Security Audit and Penetration Test](https://phpsos.de/en/leistungen/sicherheitsaudit-penetrationstest/).


## Frequently asked questions

**Is SameSite=Lax enough as CSRF protection?**  
No. According to OWASP, SameSite cookies are an additional layer, not a replacement. Lax still sends cookies on normal page requests via GET, and attacks from subdomains of the same site are not covered.

**Do JSON APIs need a CSRF token?**  
If the API authenticates with cookies, yes, or an equivalent check. An alternative is custom request headers, which browsers allow on foreign pages only after a CORS preflight. In that case, however, the CORS allowance may contain only explicitly trusted origins.

**Does the login form need protection too?**  
Yes. With login CSRF, a victim is silently logged into someone else's account and enters their own data there. A token in the login form and a new session after login prevent this.

**Does CSRF protection also protect against XSS?**  
The other way round: an XSS vulnerability defeats any CSRF protection, because injected code can read valid tokens in the name of the page and send them along. Both weaknesses must be fixed separately.

## Sources

- [OWASP Cross-Site Request Forgery Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
- [PHP manual: session configuration](https://www.php.net/manual/de/session.configuration.php)
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
