View Single Post
Limited edition
Moff's Avatar
Starter med svar til Stingray:

Sitat av Stingray Vis innlegg
Og så er spørsmålet til Moff:

Hvorfor bruker du <br><br> og ikke <p>? Er det en spesiell grunn, eller var det bare en tilfeldighet?
Vis hele sitatet...
Det er litt hipp som happ, men jeg bruker <br> bevisst for denne typen output. <p> er en avsnitt-blokk, som egentlig ikke handler om å lage linjeskift eller marginer - det er for å dele opp tekstblokker. <br> er linjeskift-kode, så når jeg vil ha linjeskift bruker jeg alltid <br>. I de fleste av mine nettsider vil jeg også ha CSS-kode knyttet til <p>-elementet som kan være uheldig, eller i verste fall føre til at jeg ikke får noe mellomrom i det hele tatt.

Sitat av Stingray Vis innlegg
sha1 er det du bør bruke, om ikke noen andre har noe bedre, så klart. Det går jo også an å kombinere flere krypteringer sammen.
Vis hele sitatet...
SHA-1 er minst like deprecated som det MD5 er. For PHP i 2019 så er det Argon2 eller Blowfish som gjelder. Argon2 er den nyeste algoritmen og det er denne man bør bruke hvis den er tilgjengelig. I de fleste PHP-installasjoner akkurat nå, så er det imidlertid Blowfish som er det beste alternativet.

Når man bruker password_hash(), så kan du bestemme hvilken algoritme som skal brukes. I kodeeksemplene deres ovenfor så skriver dere kun PASSWORD_DEFAULT, som lar PHP selv velge hvilken algoritme som skal benyttes. Hvis du vil ha kontroll på dette selv, så kan du skrive enten PASSWORD_BCRYPT (for å bruke Blowfish) eller PASSWORD_ARGON2I for å bruke Argon2. Det er også en nyere versjon av Argon2 tilgjengelig, med konstanten PASSWORD_ARGON2ID. Men som sagt, PHP-installasjonen din må være kompilert med støtte for Argon2 for at denne algoritmen skal være tilgjengelig. I skrivende stund så er Blowfish også ansett for å være trygt nok.

MD5 og SHA-1 skal imidlertid ikke benyttes, de regnes som utrygge. Årsaken til at det er sånn handler om hvordan hashing fungerer, så la oss ta et lite krasjkurs i nettopp det.

(Og denne delen er høyst relevant for trådstarter også).

Først og fremst; hashing er ikke det samme som kryptering. Når vi snakker om kryptering, så tenker vi på innhold som er maskert på en måte som gjør det svært vanskelig eller umulig å tyde - men innhold som er kryptert kan de-krypteres for å bli leselig igjen. Innhold som er hashet kan aldri "de-hashes". Jeg pleier å forklare dette med analogien om et papirark. Når du hasher noe så er det som om du krøller sammen et papir. Du kan aldri anti-krølle det; det er permanent ødelagt. Samme hvor mye du prøver, så vil papiret alltid være krøllet. Hele trikset med en hashing-algoritme, være seg MD5, SHA-1, Blowfish eller Argon2, er at det "krøller papiret" på nøyaktig samme måte, hver eneste gang.

Hvis du skriver inn samme passord, bruker samme hash-algoritme, så skal du alltid få nøyaktig samme hash-resultat. Hver gang. Og nå hører jeg trådstarter si "jammen, når jeg bruker password_hash() så får jeg ulike hasher med samme passord!!". Ja, det gjør du nok - men det handler ikke om selve hashing-algoritmen, det handler om et annet sikkerhetstiltak som vi kaller salting.

Vitsen med hashing er at vi som webmastere skal kunne verifisere brukernes passord uten at vi på noe tidspunkt trenger å vite hva passordet deres egentlig er. Vi får passordet inn, hasher det, og så sjekker vi om hashen vi fikk matcher den hashen vi har lagret fra før. Hvis begge er like, så vet vi at brukeren har tastet inn korrekt passord. Vi vet ikke hva passordet er, men vi vet at det er riktig.

La oss nå si at en hacker får tilgang til databasen din, og stjeler alle brukerne dine. Hackeren kan ikke se hva passordene til noen er, fordi de har bare en haug med ubrukelige hasher. Det hackeren imidlertid kan gjøre er å lage et skript som genererer hasher for alle mulige passord, og deretter sammenligne hashene fra databasen din og se om de tilfeldigvis finner en match. Det er jo ganske enkelt å gjøre, og det er her salting kommer inn i bildet. Salting er en teknikk hvor du tilsetter et "salt" i brukernes passord, som er en tilfeldig generert string. Hvis passordet mitt er "asdf", så kan du tilsette saltet "1234", sånn at passordet mitt blir "asdf1234". Hashen for mitt passord vil da ikke lengre matche "asdf", den matcher kun "asdf1234". Hverken du, jeg eller hackeren vet hva saltet er for noe, det er generert i PHP-koden et sted, urørt av menneskehender. Og selv om noen vet hva saltet er, så vet man ikke hvordan det blir implementert. Kanskje det er "asdf1234", kanskje det er "1234asdf", eller noe mer komplisert. Poenget er i alle fall at en hacker ikke vil klare å finne ut hva passordet er, fordi de ikke finner noen match mot hashen som gir mening.

Dette er grunnen til at du tilsynelatende får ulike resultater fra password_hash(); den salter automatisk. Resultatet av hashing-algoritmen vil faktisk inneholde informasjon om hvilken algoritme som er brukt, og hva saltet er. Det er ikke lett for oss mennesker å se det, men hvis du har drevet mye med hashing, så vil du kunne kjenne igjen algoritmen bare ved å se på prefiksen til hashen du får. Blowfish er veldig lett å kjenne igjen, fordi hashen starter med $2y$-prefiksen. Dette må til for at password_verify skal kunne skjønne hvordan sammenligning av hash og passord skal foregå, sånn at den bruker korrekt algoritme og korrekt salt. Hvis den ikke gjør det, så vil du jo som du har merket, få problemer med å sammenligne hashene selv om passordet er korrekt.

Bonusinfo: Hvorfor er MD5 og SHA-1 ikke lengre anbefalt, hvis de i prinsippet gjør akkurat det samme som Blowfish og Argon2? Enkelt og greit: Kollisjoner. En MD5- og SHA-1-hash har en fast lengde på X antall tegn. Derfor vet vi at flere ulike passord nødvendigvis MÅ resultere i nøyaktig samme hash, fordi det finnes uendelig mange unike passord i verden, men fordi hashen har en fast lengde, så er det et begrenset antall unike hasher. Etter hvert som datamaskinene våre ble raskere og raskere, så ble det lettere og lettere å generere oppslagsverk for alle mulige MD5- og SHA-1-hasher, og dermed tilsvarende lett å knekke dem. Blowfish og Argon2 er mye mer kompliserte, og er derfor langt mindre sårbare for denne typen angrep. De er ikke immune mot det, men det er trygge "nok". Inntil videre. MD5 og SHA-1 hadde heller ikke noen integrert salting-funksjon, så vi utviklere måtte implementere dette selv. Det slipper vi med de moderne algoritmene.

Det er 3 PHP-funksjoner vi må ha oversikt over:
password_hash() - Genererer hashen med valgt algoritme og automatisk salting. Brukes når passordet lagres eller må re-hashes.
password_verify() - Sammenligner et passord fra brukeren med en hash du har lagret. Brukes når man logger inn.
password_needs_rehash() - Kan alternativt brukes for å sjekke om et passord bør re-hashes, når en ny og bedre hash-algoritme har blitt tilgjengelig. Dette er sjeldne greier, men lurt å implementere hvis du bruker PASSWORD_DEFAULT (som automatisk velger den beste algoritmen). Hvis du da har brukt Blowfish, og Argon2 plutselig blir tilgjengelig for deg, så kan du automatisk oppgradere hashen til brukerne dine neste gang de logger på.

Jeg har nevnt PDO en gang tidligere her, og jeg er litt på nippet til å anbefale deg å skifte fra MySQLi til PDO. Det er fordeler og ulemper med begge to, men min profesjonelle mening er at PDO er det bedre valget. Én ting som sikkert blir tydelig for deg allerede er at prepared statements i MySQLi er et salig rot.

Kode

$sql = "SELECT id FROM login WHERE username = ? AND password = ?";
Prepared statements må du bruke for å sikre deg mot SQL-injections, slik at du skriver spørringen først med ? som placeholder for variabler, og deretter bruker du bind_param() for å erstatte placeholderne (?) med de verdiene du skal ha. Bare for å ha nevnt det; du bruker kun prepared statements når du har input fra en bruker som du ikke vet hva er; du trenger selvsagt ikke bruke prepared statements hvis du har en spørringen uten variabler, eller med variabler som du er 110% sikker på at er trygge å bruke. Prepared statements bruker bittelitt lengre tid på å kjøre enn rene spørringer, så derfor bør du ikke bruke det mer enn du må.

I PDO så er systemet ganske likt, men du har tilgang til named parameters i prepared statements. Spørringene mine ser derfor slike ut:

Kode

$sql = "SELECT id FROM login WHERE username = :username AND password = :password";
Forskjellen er at jeg kan skrive hva placeholderne skal hete, og det gjør koden MYE mer oversiktelig. I PDO trenger du heller ikke dille med datatyper; det håndteres automatisk.

MySQLi:

Kode

$stmt->bind_param("ss", $input_username, $input_password);
PDO:

Kode

$stmt->bind_param(":username", $input_username);
$stmt->bind_param(":password", $input_password);
Ikke noe "ss" for "string, string". Bare plopp inn variablene og kjør på. Du kan også binde i en hvilken som helst rekkefølge, fordi du har tilgang til named parameters. I kompliserte spørringer blir det ofte tullball når du må holde orden på rekkefølgen på parameterne.

Du er sikkert ikke så interessert i å skrive om alt sammen til PDO nå, men det er i alle fall noe å tenke over til senere. PDO har andre fordeler også, men det kan vi ev. ta ved en senere anledning.

[quote=Nikon01;3504305]@Yochi
Så en plass inni her ligger det noe jeg ikke helt skjønner kan forårsake en 500 error.
[/code]

HTTP 500 kommer som regel hvis du har en syntaksfeil i koden eller det genereres en exception som du ikke håndterer med try/catch. Typisk så betyr det mangel på en ; et sted i koden, eller at du har en syntaksfeil i en SQL-spørring.

På de fleste servere har du mulighet til å skru på output for feilmeldinger med disse linjene i starten av koden din:

Kode

ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
Dette skal du ikke gjøre i et produksjonsmiljø (ting som er åpen på internett), fordi feilmeldingene kan inneholde sensitiv informasjon som hackere ikke bør få tak i. Filnavn, spørringer - slike ting.

Det kan være greit for å debugge, siden du ser nøyaktig hvor problemet oppstår.

Som en kommentar til den siste koden du postet; jeg pleier aldri å bry meg om å sjekke om prepare() gikk bra. Min antakelse er at prepare() alltid går bra så lenge SQL-en er gyldig (ingen syntaksfeil) og forbindelsen til databasen er OK. Hvis du får til å koble til databasen, så vil prepare() gå bra i 99,99% av alle tilfeller. Jeg tenker nå på at du skriver en if() rundt prepare() - det virker litt redundant for meg. Hver sin smak, altså - feilhåndtering er vel og bra, men det kan bli for mye av det gode også.

Fordi vi ikke liker å gi løsninger direkte, så har jeg laget et eksempel på login med PDO i stedet for MySQLi:

Kode

<?php

// Database-konfig
$dbHost = 'localhost';
$dbDatabase = 'minDatabase';
$dbUsername = 'mittBrukernavn';
$dbPassword = 'mittPassord';

// Koble til med PDO, sett charset til UTF-8 (fordi vi liker UTF-8)
try {
	$db = new PDO("mysql:host=$dbHost; charset=utf8; dbname=$dbDatabase", $dbUsername, $dbPassword, array(
		PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // Sett error-mode til exception
		PDO::ATTR_EMULATE_PREPARES => false // Ikke bruk emulate prepares (få/ingen systemer gjør dette lengre, men det er greit å si ifra om det likevel)
	));
} catch(PDOException $e) {
	// Klarte ikke koble til, feilmelding tilgjengelig i $e->getMessage()
	echo 'Whops!';
	die(); // Stopp skriptet - ikke helt stuerent å gjøre dette, men det går an hvis man vil
}

if(!isset($_POST['username']) || !isset($_POST['password'])) {
	// Mangler brukernavn og/eller passord
	http_response_code(400); // Send HTTP 400 Bad request
	echo 'Ikke riktig brukernavn og/eller passord!';
	die();
}

// Hent ut passord-hashen for å sammenligne
$query = $db->prepare('SELECT id, username, password FROM users WHERE username LIKE :username'); // Prepare statement
$query->bindParam(':username', $_POST['username']); // Putt inn variabler
$query->execute(); // Utfør spørring mot databasen

// Du kan nå sjekke row count med $query->rowCount();

$row = $query->fetch(PDO::FETCH_ASSOC); // Hent første rad som et "associative array" (assoc)

if(!$row || strtolower($row['username']) != strtolower($_POST['username']) || !password_verify($_POST['password'], $row['password'])) {
	// Feil brukernavn eller feil passord
	http_response_code(401); // Send HTTP 401 Forbidden
	echo 'Ikke riktig brukernavn og/eller passord!';
	die();
}

$_SESSION['userId'] = $row['id']; // Brukeren er verifisert, logg på!
echo 'OK';

?>
Jeg foretrekker å skrive objektorientert i PHP, så jeg liker ikke å bruke die() for å stoppe skriptet midt i. Jeg pleier heller å designe koden sånn at ting flyter uten die(). Smak og behag - har ikke så fryktelig mye å si. Annet enn å øke lesbarheten.

Legg merke til hvordan jeg henter brukernavnet; jeg hater nettsider som er case-sensitive på sjekk av brukernavn! Brukernavnet mitt har stor forbokstav, men jeg vil kunne logge inn med brukernavnet i små bokstaver. Det er en filledetalj, men det er viktig for meg. LIKE er i utgangspunktet ment for søk med wildcards, men du kan også bruke den for å gjøre case-insensitive sammenligninger. Jeg vet rett og slett ikke hvorvidt LIKE er 100% nøyaktig med alle typer tegn, derfor har jeg en ekstra sjekk i koden hvor vi sammenligner $_POST['username'] med $row['username'], etter å ha brukt strtolower() på begge to (konvertert til lower case).

Du bør også sette opp brukertabellen din med en UNIQUE-key i username-kolonnen, slik at du er 100% sikker på at ingen brukere kan ha samme brukernavn.

Helt til slutt, så er det korrekt som du sier at get_result() kun er tilgjengelig med native driver. Det er pussig hvis serveren din ikke har det installert; det kan du kanskje fikse via cPanel eller tilsvarende programvare. Hvis du har SSH-tilgang med sudo kan du også installere det selv der.