# Lagra lösenord säkert i PHP: använd password_hash rätt

> Lagra lösenord säkert i PHP med password_hash: bcrypt eller Argon2id, kostnadsfaktor, gränsen på 72 byte och övergång från gamla MD5-hashar.

Källa: https://phpsos.de/sv/blog/passwoerter-in-php-speichern/  
Aktualitet: 2026-10-25  
Leverantör: PHP SOS, PHP-nödhjälp och serversäkerhet. Omedelbar hjälp via WhatsApp: https://wa.me/491736494914?text=Hej%20PHP%20SOS%2C%20vi%20beh%C3%B6ver%20hj%C3%A4lp%20med%20v%C3%A5r%20PHP-server.%20F%C3%B6retag%3A%20%E2%80%A6%20Webbplats%3A%20%E2%80%A6

**Kort sagt:** Lösenord lagrar ni i PHP med `password_hash()` och kontrollerar dem med `password_verify()`. Standarden `PASSWORD_DEFAULT` använder för närvarande bcrypt, sedan PHP 8.4 med kostnadsfaktor 12; där PHP är kompilerad med Argon2 är `PASSWORD_ARGON2ID` ett bra alternativ. Gamla MD5- eller SHA-1-hashar ställer ni om genom att omedelbart bädda in dem i password_hash och vid nästa inloggning ersätta dem med en direkt hash.

## Vad ett databasläckage har med era hashar att göra

Efter ett intrång i en webbserver är databasen ett av de första målen. Den som, som är möjligt med PHP2Shell, kan köra kod på servern läser också applikationens inloggningsuppgifter och därmed tabellen med användarkontona. Om lösenorden i den blir läsbara efter timmar eller efter år avgörs inte på angreppsdagen utan vid programmeringen.

Förvånansvärt många applikationer lagrar fortfarande lösenord med metoder som aldrig var avsedda för det ändamålet: MD5, SHA-1, en enkel SHA-256 eller till och med reversibelt krypterade. Dessa metoder är snabba, och det är just problemet. Angripare kan med dem pröva väldigt många kandidater per sekund.

PHP har sedan länge en lösning med sig som räknar långsamt, automatiskt skapar ett salt för varje lösenord och lagrar metod och parametrar i själva hashen. Ni behöver bara använda den konsekvent.

## password_hash i PHP: bcrypt eller Argon2id?

`password_hash()` känner till flera metoder. Urvalet enligt PHP-manualen:

| Konstant | Metod | Anmärkning |
|---|---|---|
| `PASSWORD_DEFAULT` | för närvarande bcrypt | Standard, kan ändras i framtida versioner |
| `PASSWORD_BCRYPT` | bcrypt | Kostnadsfaktor sedan PHP 8.4 som standard 12, tidigare 10; bara de första 72 byten räknas |
| `PASSWORD_ARGON2I` | Argon2i | sedan PHP 7.2, bara om PHP är byggd med Argon2-stöd |
| `PASSWORD_ARGON2ID` | Argon2id | sedan PHP 7.3, samma förutsättning |

OWASP rekommenderar Argon2id som förstahandsval, med minst 19 MiB minne, två genomlöpningar och en tråd, och bcrypt framför allt för befintliga system. För de flesta PHP-applikationer är skillnaden i praktiken mindre än skillnaden mellan någon av dessa metoder och MD5. Viktigare är att ni över huvud taget använder någon av dem och anpassar parametrarna regelbundet.

Om Argon2id är tillgängligt visar ett kort test:

```php
<?php
var_dump(defined('PASSWORD_ARGON2ID'));
var_dump(password_algos());
```

## Lagra och kontrollera: ett komplett mönster

Följande exempel visar registrering och inloggning med automatisk uppdatering av föråldrade hashar. Längdkontrollen skyddar mot bcrypts gräns på 72 byte:

```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()` räknar byte, inte tecken. Det är avsiktligt här, eftersom bcrypts gräns avser byte. Kolumnen i databasen bör enligt manualen rymma mer än 60 tecken, 255 är ett bra val. Sätt aldrig ett eget salt: Alternativet är föråldrat och ignoreras sedan PHP 8.0.

Vid övergången till PHP 8.4 stiger standardkostnadsfaktorn för bcrypt från 10 till 12. Befintliga hashar höjs tack vare `password_needs_rehash()` automatiskt vid nästa inloggning. På hårt belastade system är ett kort belastningstest av inloggningen värt besväret före övergången.

> **Tips:**
> Mät kostnadsfaktorn på er produktionsserver. En enskild kontroll bör kosta märkbar beräkningstid men ligga klart under en sekund, så att inloggningar inte blir en flaskhals under belastning.

## Ersätta gamla MD5- och SHA-1-hashar

Den egentliga utmaningen är oftast det befintliga beståndet. Själva lösenorden känner ni inte till, så ni kan inte bara hasha om dem. Att vänta tills varje användare loggar in igen lämnar inaktiva konton oskyddade. OWASP beskriver därför ett tvåstegsförfarande:

1. **Bädda in omedelbart:** Omsluta varje befintligt MD5- eller SHA-1-värde med `password_hash()` via skript och lagra det med en markering, till exempel i en kolumn `hash_typ`.
2. **Kontrollera vid inloggning:** För markerade konton räknar ni först om inmatningen med den gamla metoden och kontrollerar sedan med `password_verify()` mot det omslutna värdet.
3. **Ersätt direkt:** Efter lyckad inloggning skapar ni en normal hash av klartexten och tar bort markeringen.
4. **Städa upp resten:** Konton som efter en fastlagd frist fortfarande har omslutna hashar sätter ni på lösenordsåterställning.

```php
<?php
// Steg 1 (en gång via skript): $neu = password_hash($alterMd5Wert, PASSWORD_DEFAULT);
// Steg 2 och 3 vid inloggning:
if ($konto['hash_typ'] === 'md5-umhuellt') {
    $ok = password_verify(md5($eingabe), $konto['passwort_hash']);
    if ($ok) {
        $speichere(password_hash($eingabe, PASSWORD_DEFAULT), 'direkt');
    }
}
```

Omslutna hashar är bättre än nakna MD5-värden men svagare än direkta hashar. De är en övergångslösning, inget mål. Efter en incident där databasen kan ha berörts ersätter den här omställningen inte uppmaningen till användarna att byta sina lösenord. Vilka spår som tyder på en sådan incident beskriver [Är jag redan komprometterad?](https://phpsos.de/sv/php2shell/bin-ich-gehackt-anzeichen/).

## Pepper, kryptering och andra villospår

Vissa idéer låter som mer säkerhet men ger lite eller skadar:

- **Egna hashkonstruktioner** som upprepad SHA-256 med självskapat salt är snabbare än bcrypt och därför svagare.
- **Krypterade lösenord** är reversibla. Ligger nyckeln på samma server skyddar krypteringen knappast efter ett intrång.
- **En pepper**, alltså ett hemligt tilläggsvärde för alla lösenord, kan vara ett extra lager om den förvaras skild från databasen. Den ersätter dock inte en bra hashmetod och försvårar senare byten.
- **Förhashning före bcrypt** för att kringgå gränsen på 72 byte medför enligt OWASP egna risker. En tydlig längdgräns eller Argon2id är enklare.

Också för reglerna kring nya lösenord är det värt att titta på det aktuella läget inom tekniken: Längd skyddar mer än invecklade föreskrifter om specialtecken, och mycket vanliga lösenord eller sådana som redan förekommit i dataläckor bör avvisas. Påtvingade byten med fasta intervall utan anledning leder erfarenhetsmässigt snarare till förutsägbara mönster än till starkare lösenord.

För inloggningssystem hör lösenord dessutom aldrig hemma ensamma: Tvåfaktorsmetoder för administratörer och en broms mot massiva inloggningsförsök kompletterar lagringen. Båda behandlas i guiden [Skydda admininloggningar mot brute force](https://phpsos.de/sv/ratgeber/brute-force-login-schutz/). För återställningslänkar och engångskoder gäller andra regler, se [Säkra slumptal och tokens i PHP](https://phpsos.de/sv/blog/sichere-tokens-php/). En översikt över ytterligare funktioner ger [PHP:s inbyggda säkerhetsfunktioner](https://phpsos.de/sv/php/eingebaute-sicherheitsfunktionen/).

## Hur hjälper PHP SOS konkret?

- Vi kontrollerar hur er applikation och era tillägg lagrar lösenord och hittar föråldrade metoder även i sidofunktioner.
- Vi planerar omställningen av beståndet med omslutning, inloggningsmigrering och frist för återstående konton.
- Vi anpassar kostnadsfaktor eller Argon2-parametrar till era servrar.
- Efter en incident stöder vi beslutet om lösenordsåterställningar och deras tekniska genomförande.

En granskning av egen PHP-programvara erbjuder vi som [säkerhetsaudit och penetrationstest](https://phpsos.de/sv/leistungen/sicherheitsaudit-penetrationstest/).


## Vanliga frågor

**Vilken algoritm använder password_hash med PASSWORD_DEFAULT?**  
För närvarande bcrypt. PHP-manualen påpekar att standarden kan ändras när starkare metoder tillkommer. Därför bör databaskolumnen rymma mer än 60 tecken; manualen anger 255 som ett bra val.

**Bör jag kryptera lösenord i stället för att hasha dem?**  
Nej. Krypterade lösenord går att läsa igen med nyckeln, och den nyckeln ligger oftast på samma server. En lösenordshash är enkelriktad: Applikationen kan bara kontrollera om en inmatning stämmer.

**Hur hög bör kostnadsfaktorn vara för bcrypt?**  
Sedan PHP 8.4 är standarden 12, tidigare 10. OWASP anger 10 som nedre gräns och rekommenderar att gå så högt som era servrar tål. Mät på den egna hårdvaran hur lång tid en kontroll tar.

**Vad betyder gränsen på 72 byte för bcrypt?**  
bcrypt beaktar bara de första 72 byten i ett lösenord, allt därefter kapas. Med å, ä och ö räknas ett tecken ofta som två byte. Begränsa därför längden uttryckligen eller använd Argon2id om er PHP är kompilerad med det.

**Måste användare byta lösenord efter ett intrång?**  
Om det inte kan uteslutas att databasen har läckt bör ni föranstalta det. Bra hashar fördröjer knäckning avsevärt, men svaga lösenord förblir angripbara. Om det dessutom finns anmälningsskyldighet klargör ni med ert dataskyddsombud.

## Källor

- [PHP-manualen: 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/)
