Kort sagt: PHP type juggling betegner den automatiske typeomdannelse, der ved den løse sammenligning med == afgør resultatet. Siden PHP 8.0 er reglerne strengere, men numeriske strenge sammenlignes fortsat som tal, og true, false og null følger egne regler. Til sikkerhedsrelevante kontroller gælder derfor: ===, in_array(..., true), match i stedet for switch, hash_equals() til tokens og password_verify() til adgangskoder.
En sammenligning, der er sand, selv om den ikke burde være det
Mange sårbarheder i PHP-applikationer sidder ikke i komplicerede funktioner, men i ét enkelt tegn: et ==, hvor der burde have stået ===. Koden ser korrekt ud, tests med normale input kører igennem, og alligevel lader kontrollen under bestemte betingelser noget ske, som den skulle have stoppet.
Årsagen er en egenskab ved PHP, som i hverdagen er praktisk: Værdier omdannes efter behov til en anden type. En streng fra en formular kan således uden omvej sammenlignes med et tal fra databasen. Ved logins, tokens, roller eller statusfelter kan netop denne gavmildhed blive farlig.
Hvordan PHP sammenligner med ==
PHP-manualen beskriver reglerne for løse sammenligninger udførligt. For sikkerheden er især disse tilfælde vigtige:
| Sammenligning | PHP 7 | PHP 8 | Årsag |
|---|---|---|---|
0 == "a" |
sand | falsk | siden PHP 8.0 sammenlignet som strenge |
"1" == "01" |
sand | sand | begge numeriske, sammenligning som tal |
"10" == "1e1" |
sand | sand | eksponentiel notation er numerisk |
100 == "1e2" |
sand | sand | tal mod numerisk streng |
null == false |
sand | sand | sammenligning som sandhedsværdi |
"abc" == true |
sand | sand | ikke-tom streng gælder som sand |
Ændringen i PHP 8.0 har afhjulpet en hel klasse af fejl: Tidligere blev en ikke-numerisk streng ved sammenligning med et tal til 0. Det berørte også switch-sætninger. De øvrige rækker i tabellen viser dog, at løse sammenligninger fortsat rummer overraskelser.
Også operatorerne <, > og sammenligningsoperatoren <=> følger disse regler. Ved sorteringer er det som regel harmløst, ved kontroller som "beløb mindre end grænse" med værdier fra forespørgslen bør typen stå fast på forhånd.
Særligt lumsk er rækken med den eksponentielle notation. Hash-værdier og tokens består ofte af cifre og bogstaver. Ligner en sådan værdi tilfældigt et tal i eksponentiel notation, sammenligner == to forskellige hashes måske som ens. Det er baggrunden for mange rapporter om omgåelse af login i ældre PHP-applikationer.
PHP type juggling i typiske sikkerhedskontroller
Løse sammenligninger skjuler sig på flere steder end blot i operatoren ==:
in_array()ogarray_search()sammenligner uden tredje parameter løst. En tilladelsesliste med roller eller ID'er bliver dermed blødere end tænkt.switchsammenligner løst. Udtrykketmatch, tilgængeligt siden PHP 8.0, sammenligner ifølge manualen derimod typesikkert med===og kaster en fejl, hvis ingen gren passer.- JSON-input leverer ægte typer. Et felt, I forventer som streng, kan ankomme som
trueeller som tal og med==overraskende stemme overens. strcmp()til hemmeligheder er uegnet, fordi køretiden afhænger af indholdet, og fejltilfælde let vurderes forkert.- Kontrol af returværdier: Funktioner som
strpos()returnerer0for et fund i begyndelsen ogfalsefor intet fund.if (!strpos(...))forveksler begge.
Sikre sammenligninger: opstillingen
Følgende eksempler viser hver gang den usikre og den sikre variant:
<?php
declare(strict_types=1);
// Kontrollér rolle
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // usikker: løs
in_array($rolle, $erlaubteRollen, true); // sikker: streng
// Vurdér status
$aktion = match ($status) { // streng, fejl ved ukendt værdi
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Kontrollér token fra link
if ($tokenAusLink == $gespeichertesToken) { /* usikker */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* sikker */ }
// Kontrollér adgangskode
if (md5($eingabe) == $hash) { /* usikker: svag metode og løs sammenligning */ }
if (password_verify($eingabe, $hash)) { /* sikker */ }
// Søg delstreng
if (strpos($text, 'abc') !== false) { /* korrekt: også fund på position 0 */ }
hash_equals() forventer ifølge manualen den kendte hemmelighed som første og brugerens værdi som anden parameter og sammenligner i konstant tid. Hvordan I overhovedet genererer tokens sikkert, beskriver Sikre tilfældige tal og tokens i PHP.
declare(strict_types=1) gør ikke sammenligninger strikse. Erklæringen berører kun typeomdannelsen ved funktionskald og returværdier. Løse sammenligninger skal I selv erstatte i koden.
Strikse sammenligninger har en bagside: De mislykkes, når typerne ikke passer sammen. '42' === 42 er falsk. Værdier fra formularer og URL'er ankommer i PHP som strenge, værdier fra databasen afhængigt af driver og indstilling som strenge eller tal. Den, der indfører === overalt uden at tilpasse typerne, skaber fejl, som i driften falder i øjnene som afvisning af berettigede brugere.
Løsningen er en klar grænse: Input valideres ved modtagelsen og bringes til den rigtige type, f.eks. med filter_var($wert, FILTER_VALIDATE_INT), og behandles derefter i koden kun med typedeklarationer og strikse sammenligninger. Typedeklarationer på parametre, returværdier og egenskaber hjælper hertil, fordi forkerte typer så tidligt viser sig med en fejlmeddelelse i stedet for stille at forsvinde i en sammenligning.
Gennemgå eksisterende kode systematisk
I gamle projekter findes løse sammenligninger i hundredvis. Ikke hver eneste er et problem. Koncentrér jer om steder, hvor der træffes en beslutning om adgang:
# Løse sammenligninger i linjer med sikkerhedsrelevante begreber
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array uden streng sammenligning
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Værktøjer til statisk analyse som PHPStan eller Psalm kan på forlangende varigt melde løse sammenligninger. Så kommer nye tilfælde slet ikke ind i hovedgrenen. Grundlæggende om typer beskriver PHP's typesystem; hvordan input på forhånd bringes til den rigtige type, viser Validér og filtrér input i PHP.
Hvad det betyder for ældre systemer
Applikationer, der stadig kører på PHP 7, er berørt af de gamle, mere slappe sammenligningsregler. Dertil kommer et andet problem: For PHP 8.1 og ældre findes der fra PHP-projektet ikke længere officielle sikkerhedsopdateringer, heller ikke mod PHP2Shell. Situationen beskriver PHP 8.1 og ældre: Hvad der gælder nu.
Ved en migrering til PHP 8 ændrer nogle sammenligningsresultater sig. Det er som regel en forbedring, men kan ødelægge kode, der har stolet på den gamle adfærd. Planlæg derfor tests, der målrettet dækker sammenligninger med tal, tomme værdier og null.
Hvordan hjælper PHP SOS konkret?
- Vi gennemsøger jeres kode efter løse sammenligninger på steder, der afgør login, rettigheder eller betalinger.
- Vi erstatter dem med strikse sammenligninger,
match-udtryk og passende kontrolfunktioner. - Vi sætter statisk analyse op, så nye løse sammenligninger opdages i buildet.
- Vi ledsager migreringer til aktuelle PHP-versioner med tests for ændrede sammenligningsregler.
En kontrol af jeres software beskriver Sikkerhedsaudit og penetrationstest.
Ofte stillede spørgsmål
Hvad er type juggling i PHP?
PHP omdanner ved visse operationer automatisk værdier af forskellig type, f.eks. en streng til et tal. Ved den løse sammenligning med == afgør denne omdannelse resultatet. Det er bekvemt, men kan føre til sammenligninger, der er sande, selv om værdierne ikke ser ens ud.
Har PHP 8 løst problemet?
Delvist. Siden PHP 8.0 sammenlignes et tal med en ikke-numerisk streng som streng, så 0 == 'a' er ikke længere sandt. To numeriske strenge sammenlignes dog fortsat som tal, og sammenligninger med true, false og null følger egne regler.
Hjælper declare(strict_types=1) mod løse sammenligninger?
Nej. strict_types styrer, om skalare typer omdannes automatisk ved funktionskald og returværdier. På sammenligningsoperatorerne == og != har det ingen indflydelse.
Hvordan sammenligner jeg adgangskoder og tokens rigtigt?
Adgangskoder kontrollerer I med password_verify, tokens og signaturer med hash_equals. Begge sammenligner præcist og, ved hash_equals, i konstant tid. En sammenligning med == eller strcmp er uegnet hertil.