# CSRF-beskyttelse i PHP-applikationer

> Gennemfør CSRF-beskyttelse i PHP: synchronizer-token, kontrol af Origin og Sec-Fetch-Site, SameSite-cookies og login-CSRF. Med kodeeksempel og tjekliste.

Kilde: https://phpsos.de/da/blog/csrf-schutz-php/  
Status: 25.10.2026  
Udbyder: PHP SOS, PHP-nødhjælp og serversikkerhed. Akut hjælp på WhatsApp: https://wa.me/491736494914?text=Hej%20PHP%20SOS%2C%20vi%20har%20brug%20for%20hj%C3%A6lp%20med%20vores%20PHP-server.%20Firma%3A%20%E2%80%A6%20Website%3A%20%E2%80%A6

**Kort sagt:** CSRF i PHP forhindrer I ved, at hver forespørgsel, der ændrer noget, skal medbringe et hemmeligt token bundet til sessionen, som I kontrollerer på serversiden med `hash_equals()`. Desuden kontrollerer I headerne `Origin` eller `Sec-Fetch-Site` og sætter session-cookies med `SameSite`. GET-forespørgsler må aldrig ændre data, og loginformularen har også brug for beskyttelse.

## Et klik, ingen lægger mærke til

En medarbejder er logget ind i shoppens backend og åbner en fremmed side i en anden fane. Denne side sender i baggrunden en formular til backenden, f.eks. for at ændre en e-mailadresse eller oprette en konto. Browseren hænger automatisk session-cookien på. For applikationen ser forespørgslen ud som enhver anden fra eget team.

Det er Cross-Site Request Forgery, kort CSRF: en forespørgsel, som en logget-ind bruger uvidende sender til en applikation, vedkommende stoler på. Angriberen ser ikke svaret, men har heller ikke brug for det. Det er nok, at handlingen udføres.

CSRF står ikke i overskrifterne som PHP2Shell, men er et hyppigt fund i selvudviklede PHP-applikationer. Frameworks som Symfony og Laravel medbringer en beskyttelse; sårbarheder opstår dér især, hvor den er slået fra for enkelte ruter. Egenudviklinger uden framework har ofte slet ingen.

## CSRF i PHP: Hvilke forespørgsler skal beskyttes

Beskyttelse har alle forespørgsler brug for, der ændrer en tilstand: gemme, slette, bestille, tildele rettigheder, ændre indstillinger, uploade filer, logge ud. Læsende kald behøver ikke noget token, så længe de virkelig kun læser.

Heraf følger den første regel: **Ingen ændring via GET.** Links som `loeschen.php?id=…` kan især let udløses fra fremmede sider, f.eks. via et indlejret billede. Ændrende handlinger hører hjemme i POST, PUT, PATCH eller DELETE.

Den anden regel: **Beskyt også login.** Ved såkaldt login-CSRF bliver et offer ubemærket logget ind på en konto, angriberen ejer. Indtaster offeret dér betalingsdata eller dokumenter, havner disse hos angriberen. Et token i loginformularen og et nyt session-ID efter vellykket login forhindrer det.

## Synchronizer-token-mønstret i PHP

Til klassiske applikationer med sessioner anbefaler OWASP synchronizer-token-mønstret: Serveren genererer et tilfældigt token, gemmer det i sessionen og forventer det tilbage i hver ændrende formular. Vigtigt er, at tokenet sendes med i formularen eller i en egen header, ikke i en 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') ?>">
    <!-- flere felter -->
</form>
```

`hash_equals()` sammenligner i konstant tid, så svartiden intet afslører om den rigtige værdi. Efter login bør I fornye session-ID'et med `session_regenerate_id(true)` og generere et nyt token. Hvorfor `random_bytes()` er den rigtige kilde, og hvilke funktioner der er uegnede, forklarer [Sikre tilfældige tal og tokens i PHP](https://phpsos.de/da/blog/sichere-tokens-php/).

Ét token pr. session er nok til de fleste applikationer. Tokens pr. formular eller pr. forespørgsel øger beskyttelsen en smule, men fører til problemer med flere åbne faner og tilbage-knappen. Vigtigere end hyppigheden af skiftet er, at tokenet aldrig optræder i URL'er, hvor det kan havne i logge, i browserhistorikken og i referrer-oplysninger til fremmede sider.

Til tilstandsløse applikationer uden session nævner OWASP som alternativ den signerede double-submit-cookie: et token, der er bundet til sessionen med HMAC. Den enkle, usignerede variant anbefales udtrykkeligt ikke.

## Andet lag: Origin, Sec-Fetch-Site og SameSite

Moderne browsere sender ved forespørgsler oplysninger om deres oprindelse. Headeren `Origin` nævner den kaldende side, `Sec-Fetch-Site` angiver, om forespørgslen stammer fra den samme side (`same-origin`), en beslægtet (`same-site`) eller en fremmed (`cross-site`). Heraf kan der bygges en enkel ekstra kontrol:

```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 fremhæver her to punkter: Oprindelsen skal sammenlignes fuldstændigt, så domæner med lignende navne ikke slipper igennem, og en manglende header eller værdien `null` må ikke gælde som egen oprindelse. Fordi ældre browsere ikke altid sender headerne, forbliver tokenet hovedforsvaret.

Det tredje lag er session-cookies med `SameSite`. PHP understøtter `session.cookie_samesite` siden version 7.3, standardværdien er tom. Sæt mindst `Lax`, for rene backends er `Strict` ofte muligt. De øvrige cookie-indstillinger beskriver vejledningen [Sikr sessioner i PHP](https://phpsos.de/da/ratgeber/session-sicherheit-php/).

Bruger I et CMS, medbringer det som regel egne værktøjer. WordPress arbejder f.eks. med såkaldte nonces, som plugins indbygger via funktioner som `wp_nonce_field()` og `check_admin_referer()`. Sårbarheder opstår i praksis især i udvidelser, der glemmer sådanne kontroller ved enkelte handlinger. Kontrollér derfor særligt selvskrevne eller lidt udbredte plugins.

Ved grænseflader, som browsere bruger fra andre domæner, afgør CORS-konfigurationen desuden. Returnerer serveren enhver forespørgende oprindelse som tilladt og tillader samtidig cookies, sætter den selv browserens beskyttelse ud af kraft. OWASP anbefaler kun at åbne CORS for udtrykkeligt betroede oprindelser; en generel frigivelse af alle subdomæner bliver et problem, så snart et af dem falder i fremmede hænder.

## Tjekliste til eksisterende applikationer

Ved gamle projekter hjælper en systematisk gennemgang:

1. Registrér alle ruter og scripts, der ændrer data, også Ajax-endepunkter og filuploads.
2. Kontrollér, om der bruges GET til det, og omstil til POST eller passende metoder.
3. Påvis for hver ændrende rute, at et token kontrolleres; søg målrettet efter undtagelser i frameworket.
4. Test login, logout og ændring af adgangskode for sig.
5. Tilføj `session.cookie_samesite` og oprindelseskontrollen.
6. Udbedr XSS-sårbarheder, for indsmuglet kode kan læse ethvert token. Fremgangsmåden beskriver [Forhindr cross-site scripting i PHP](https://phpsos.de/da/ratgeber/xss-php/).

> **Vigtigt:**
> En CSRF-beskyttelse, der kun kontrolleres i frontend med JavaScript, er virkningsløs. Kontrollen skal ske på serveren, før handlingen udføres.

## Hvordan hjælper PHP SOS konkret?

- Vi registrerer alle ændrende ruter i jeres PHP-applikation og kontrollerer, hvor et token mangler eller er slået fra.
- Vi indbygger et centralt kontrolpunkt i stedet for at fordele token-kontroller over mange scripts.
- Vi tilføjer oprindelseskontrol og SameSite-indstillinger uden at forstyrre betalings- eller loginforløb.
- Vi tester igen efter omstillingen og dokumenterer resultaterne.

En fuldstændig kontrol af jeres applikation beskriver ydelsen [Sikkerhedsaudit og penetrationstest](https://phpsos.de/da/leistungen/sicherheitsaudit-penetrationstest/).


## Ofte stillede spørgsmål

**Er SameSite=Lax nok som CSRF-beskyttelse?**  
Nej. SameSite-cookies er ifølge OWASP et ekstra lag, ikke en erstatning. Lax sender fortsat cookies ved normale sidekald via GET, og angreb fra subdomæner på det samme website dækkes ikke.

**Har JSON-API'er brug for et CSRF-token?**  
Hvis API'et autentificerer med cookies, ja, eller en tilsvarende kontrol. Et alternativ er egne forespørgsels-headere, som browsere kun tillader fra fremmede sider efter en CORS-forhåndskontrol. Så må CORS-frigivelsen dog kun indeholde udtrykkeligt betroede oprindelser.

**Skal også loginformularen beskyttes?**  
Ja. Ved login-CSRF bliver et offer ubemærket logget ind på en fremmed konto og indtaster dér egne data. Et token i loginformularen og en ny session efter login forhindrer det.

**Beskytter CSRF-beskyttelse også mod XSS?**  
Omvendt: En XSS-sårbarhed sætter enhver CSRF-beskyttelse ud af kraft, fordi indsmuglet kode i sidens navn kan læse og medsende gyldige tokens. Begge svagheder skal udbedres hver for sig.

## Kilder

- [OWASP Cross-Site Request Forgery Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
- [PHP-manual: Sessionkonfiguration](https://www.php.net/manual/de/session.configuration.php)
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
