# Gem adgangskoder sikkert i PHP: Brug password_hash rigtigt

> Gem adgangskoder sikkert i PHP med password_hash: bcrypt eller Argon2id, omkostningsfaktor, 72-bytes-grænsen og overgang fra gamle MD5-hashes.

Kilde: https://phpsos.de/da/blog/passwoerter-in-php-speichern/  
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:** Adgangskoder gemmer I i PHP med `password_hash()` og kontrollerer dem med `password_verify()`. Standarden `PASSWORD_DEFAULT` bruger i øjeblikket bcrypt, siden PHP 8.4 med omkostningsfaktor 12; hvor PHP er kompileret med Argon2, er `PASSWORD_ARGON2ID` et godt alternativ. Gamle MD5- eller SHA-1-hashes omstiller I ved straks at indlejre dem i password_hash og ved næste login erstatte dem med en direkte hash.

## Hvad et databaselæk har at gøre med jeres hashes

Efter et indbrud i en webserver er databasen et af de første mål. Den, der som ved PHP2Shell kan udføre kode på serveren, læser også applikationens adgangsdata og dermed tabellen med brugerkonti. Om adgangskoderne deri bliver læsbare efter timer eller efter år, afgøres ikke på angrebsdagen, men under programmeringen.

Overraskende mange applikationer gemmer stadig adgangskoder med metoder, der aldrig var tænkt til formålet: MD5, SHA-1, en simpel SHA-256 eller endda reversibelt krypteret. Disse metoder er hurtige, og netop det er problemet. Angribere kan med dem prøve meget mange kandidater pr. sekund.

PHP medbringer for længst en løsning, der regner langsomt, automatisk genererer et salt for hver adgangskode og gemmer metode og parametre i selve hashen. I skal blot bruge den konsekvent.

## password_hash i PHP: bcrypt eller Argon2id?

`password_hash()` kender flere metoder. Udvalget ifølge PHP-manualen:

| Konstant | Metode | Bemærkning |
|---|---|---|
| `PASSWORD_DEFAULT` | i øjeblikket bcrypt | Standard, kan ændre sig i fremtidige versioner |
| `PASSWORD_BCRYPT` | bcrypt | Omkostningsfaktor siden PHP 8.4 som standard 12, tidligere 10; kun de første 72 bytes tæller |
| `PASSWORD_ARGON2I` | Argon2i | siden PHP 7.2, kun hvis PHP er bygget med Argon2-understøttelse |
| `PASSWORD_ARGON2ID` | Argon2id | siden PHP 7.3, samme forudsætning |

OWASP anbefaler Argon2id som første valg, med mindst 19 MiB hukommelse, to gennemløb og én tråd, og bcrypt først og fremmest til eksisterende systemer. For de fleste PHP-applikationer er forskellen i praksis mindre end forskellen mellem en hvilken som helst af disse metoder og MD5. Vigtigere er, at I overhovedet bruger en af dem og tilpasser parametrene regelmæssigt.

Om Argon2id er tilgængelig, viser en kort test:

```php
<?php
var_dump(defined('PASSWORD_ARGON2ID'));
var_dump(password_algos());
```

## Gem og kontrollér: et komplet mønster

Følgende eksempel viser registrering og login med automatisk opdatering af forældede hashes. Længdekontrollen beskytter mod bcrypts 72-bytes-grænse:

```php
<?php
declare(strict_types=1);

const PASSWORT_ALGO = PASSWORD_DEFAULT;

function passwortSpeichern(string $passwort): string
{
    if (strlen($passwort) > 72) {
        throw new InvalidArgumentException('Passwort zu lang für das gewählte Verfahren.');
    }
    return password_hash($passwort, PASSWORT_ALGO);
}

function passwortPruefen(string $eingabe, string $gespeicherterHash, callable $hashAktualisieren): bool
{
    if (!password_verify($eingabe, $gespeicherterHash)) {
        return false;
    }
    if (password_needs_rehash($gespeicherterHash, PASSWORT_ALGO)) {
        $hashAktualisieren(password_hash($eingabe, PASSWORT_ALGO));
    }
    return true;
}
```

`strlen()` tæller bytes, ikke tegn. Det er hensigten her, for bcrypt-grænsen refererer til bytes. Kolonnen i databasen bør ifølge manualen kunne rumme mere end 60 tegn, 255 er et godt valg. Sæt aldrig et eget salt: Indstillingen er forældet og ignoreres siden PHP 8.0.

Med skiftet til PHP 8.4 stiger standardomkostningsfaktoren for bcrypt fra 10 til 12. Eksisterende hashes løftes takket være `password_needs_rehash()` automatisk ved næste login. På stærkt belastede systemer er en kort belastningstest af login værd, før I skifter.

> **Tip:**
> Mål omkostningsfaktoren på jeres produktionsserver. En enkelt kontrol bør kunne mærkes på regnetiden, men forblive tydeligt under et sekund, så login ikke bliver en flaskehals under belastning.

## Afløs gamle MD5- og SHA-1-hashes

Den egentlige udfordring er som regel bestanden. Selve adgangskoderne kender I ikke, så I kan ikke bare hashe dem påny. At vente, til hver bruger logger ind igen, lader inaktive konti stå ubeskyttede. OWASP beskriver derfor en totrinsfremgangsmåde:

1. **Indlejr straks:** Ombryd hver eksisterende MD5- eller SHA-1-værdi med `password_hash()` via et script og gem den med en markering, f.eks. i en kolonne `hash_typ`.
2. **Kontrollér ved login:** For markerede konti omregnes inputtet først med den gamle metode og kontrolleres derefter med `password_verify()` mod den ombryggede værdi.
3. **Erstat direkte:** Efter vellykket login genereres en normal hash ud fra klarteksten, og markeringen fjernes.
4. **Ryd op i restbestanden:** Konti, der efter en fastsat frist stadig har ombryggede hashes, sættes til nulstilling af adgangskoden.

```php
<?php
// Trin 1 (engangs via script): $neu = password_hash($alterMd5Wert, PASSWORD_DEFAULT);
// Trin 2 og 3 ved login:
if ($konto['hash_typ'] === 'md5-umhuellt') {
    $ok = password_verify(md5($eingabe), $konto['passwort_hash']);
    if ($ok) {
        $speichere(password_hash($eingabe, PASSWORD_DEFAULT), 'direkt');
    }
}
```

Ombryggede hashes er bedre end nøgne MD5-værdier, men svagere end direkte hashes. De er en overgangsløsning, ikke et mål. Efter en hændelse, hvor databasen kan være berørt, erstatter denne omstilling ikke opfordringen til brugerne om at skifte deres adgangskoder. Hvilke spor der peger på en sådan hændelse, beskriver [Er jeg allerede kompromitteret?](https://phpsos.de/da/php2shell/bin-ich-gehackt-anzeichen/).

## Pepper, kryptering og andre vildspor

Nogle idéer lyder som mere sikkerhed, men giver lidt eller skader:

- **Egne hash-konstruktioner** som gentaget SHA-256 med selvgenereret salt er hurtigere end bcrypt og derfor svagere.
- **Krypterede adgangskoder** er reversible. Ligger nøglen på den samme server, beskytter krypteringen efter et indbrud næppe noget.
- **En pepper**, altså en hemmelig tilføjelsesværdi for alle adgangskoder, kan være et ekstra lag, hvis den opbevares adskilt fra databasen. Den erstatter dog ikke en god hash-metode og besværliggør senere skift.
- **Forhåndshashing før bcrypt** for at omgå 72-bytes-grænsen rummer ifølge OWASP egne risici. En klar længdegrænse eller Argon2id er enklere.

Også ved reglerne for nye adgangskoder er det værd at se på den aktuelle tekniske standard: Længde beskytter mere end indviklede krav om specialtegn, og meget almindelige adgangskoder eller adgangskoder, der allerede er dukket op i datalæk, bør afvises. Tvungne skift med faste mellemrum uden anledning fører erfaringsmæssigt snarere til forudsigelige mønstre end til stærkere adgangskoder.

For loginsystemer står adgangskoder desuden aldrig alene: Totrinsmetoder for administratorer og en bremse mod massive loginforsøg supplerer lagringen. Begge dele behandler vejledningen [Beskyt admin-logins mod brute force](https://phpsos.de/da/ratgeber/brute-force-login-schutz/). For nulstillingslinks og engangskoder gælder andre regler, se [Sikre tilfældige tal og tokens i PHP](https://phpsos.de/da/blog/sichere-tokens-php/). Et overblik over yderligere funktioner giver [PHP's indbyggede sikkerhedsfunktioner](https://phpsos.de/da/php/eingebaute-sicherheitsfunktionen/).

## Hvordan hjælper PHP SOS konkret?

- Vi kontrollerer, hvordan jeres applikation og plugins gemmer adgangskoder, og finder forældede metoder også i sidefunktioner.
- Vi planlægger omstillingen af bestanden med ombrydning, login-migrering og frist for restkonti.
- Vi afstemmer omkostningsfaktor eller Argon2-parametre med jeres servere.
- Efter en hændelse støtter vi ved beslutningen om nulstilling af adgangskoder og den tekniske gennemførelse.

En kontrol af egen PHP-software tilbyder vi som [sikkerhedsaudit og penetrationstest](https://phpsos.de/da/leistungen/sicherheitsaudit-penetrationstest/).


## Ofte stillede spørgsmål

**Hvilken algoritme bruger password_hash med PASSWORD_DEFAULT?**  
I øjeblikket bcrypt. PHP-manualen påpeger, at standarden kan ændre sig, når stærkere metoder kommer til. Derfor bør databasekolonnen kunne rumme mere end 60 tegn; manualen nævner 255 som et godt valg.

**Skal jeg kryptere adgangskoder i stedet for at hashe dem?**  
Nej. Krypterede adgangskoder kan læses igen med nøglen, og denne nøgle ligger oftest på den samme server. En adgangskode-hash er en envejsgade: Applikationen kan kun kontrollere, om et input passer.

**Hvor høj bør omkostningsfaktoren være ved bcrypt?**  
Siden PHP 8.4 ligger standarden på 12, tidligere på 10. OWASP nævner 10 som nedre grænse og anbefaler at gå så højt, som jeres servere kan klare. Mål på jeres egen hardware, hvor lang tid en kontrol tager.

**Hvad betyder 72-bytes-grænsen ved bcrypt?**  
bcrypt tager kun hensyn til de første 72 bytes af en adgangskode, alt derefter afskæres. Ved tegn som æ, ø og å tæller et tegn ofte to bytes. Begræns derfor længden udtrykkeligt, eller brug Argon2id, hvis jeres PHP er kompileret med det.

**Skal brugere skifte adgangskode efter et indbrud?**  
Hvis det ikke kan udelukkes, at databasen er sluppet ud, bør I foranledige det. Gode hashes forsinker knækningen betydeligt, men svage adgangskoder forbliver sårbare. Om der desuden består en anmeldelsespligt, afklarer I med jeres databeskyttelsesrådgiver.

## Kilder

- [PHP-manual: password_hash](https://www.php.net/manual/de/function.password-hash.php)
- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
