Historik:
19.03-2012: Tilføjet UPDATE eksempel
19.07-2012: Opdatering om Zend's anbefalinger
Zend (som vedligeholder PHP) anbefaler nu direkte, at man fravælger MySQL-API'et til nyudvikling. I stedet anbefales MySQLI eller PDO.
MySQL
Det gamle MySQL-API, som rigtig mange stadig bruger, bygger på en højst usikker tilgang til databaseforespørgsler. Det bygger på konkatenering (sammenklistring) af strengstumper til en SQL-kommando, som efterfølgende afvikles direkte mod databasen - f.eks:
$user = $_POST['username'];
$sql = "SELECT `id`, `points` FROM `mytable` WHERE `user`='.$user.' LIMIT 1";
$res = mysql_query($sql);
$row = mysql_fetch_assoc($res);
echo 'Brugeren '.$user.' med id '.$row['id'].' har '.$row['points'];
Dette er i udgangspunktet en yderst uhensigtsmæssig fremgangsmåde, eftersom variabler - som ofte kommer udefra (altså noget, jeg som udvikler ikke selv har skrevet) - bliver sendt direkte ned i databasen og afviklet mod denne.
Man kan lappe på problemet ved at escape værdier med funktioner som mysql_real_escape_string, men det er - og vil altid være - lapperier på en i udgangspunktet forkert fremgangsmåde. Da mysql_real_escape_string kun kan bruges på strenge, står løsningen stadig åben for SQL-injection, hvis der bliver brugt tal i SQL'en.
Desuden virker mysql_real_escape_string ikke på strenge i multibyte tegnsæt - med undtagelse af utf-8. Sidst, men ikke mindst, er løsningen afhængig af, at udvikleren husker at bruge den hver eneste gang - hvilket med garanti ikke sker.
Meget ofte vil mysql_real_escape_string o.lign. 'hacks' ydermere resultere i uønskede dobbelt escapings med deraf følgende backslashes (\) i de gemte data, når disse udskrives.
Desværre ses brugen af dette API ofte i lærebøger og tutorials. Lærebøger om webkode forældes hurtigt, og tutorialforfattere har ofte lært fra ældre bøger eller andre tutorials uden at opdatere deres viden.
Endnu værre er det, at der stadig undervises i API'et på flere uddannelser på beskedent niveau. Hvad det skyldes, skal jeg ikke kunne sige, men det er i hvertfald overordentlig skidt, hvis meningen er, at eleverne skal lære at skrive tidssvarende og sikker kode.
Prepared Statements
En langt bedre tilgang er at bruge prepared statements (forberedte erklæringer). Her forgår forespørgslen i følgende trin:
1) Prepare: Databasens styringsmekanisme opretter en 'skabelon' på baggrund af en SQL-streng, hvor nogle værdier er erstattet af spørgsmålstegn:
SELECT `fornavn` FROM `tabel` WHERE `id` = ?
Databasen opretter en optimeret plan for forespørgslen og gemmer denne plan - uden at gennemføre forespørgslen.
2) Bind parameters: Til den eller de værdier, der var erstattet af spørgsmålstegn i skabelonen, bindes egentlige værdier i form af f.eks. strenge eller tal. I ovenstående eksempel kunne det være tallet 123.
3) Execute: Databasen udfører forespørgslen med de bundne værdier.
Ved forespørgsler, som skal returnere et sæt af resultater/rækker, anvendes desuden følgende to trin:
4) Bind result: De fundne data bindes til en eller flere variabler. I eksemplet ovenfor kunne denne variabel f.eks. kaldes $fornavn.
5) Fetch: De fundne rækker hentes én ad gangen - lidt, som det kendes fra 'det gamle API'. Når en række er hentet, vil $fornavn (variablen fra resultatbindingen) indeholde værdien af det søgte felt i rækken.
Så længe det pågældende statement (den optimerede plan) ikke lukkes, kan punkt 2, 3, 4 og 5 udføres så mange, man måtte ønske.
Fordelen ved denne fremgangsmåde er, at værdierne ikke sendes til databasen sammen med selve SQL-kommandoen, men sendes som parametre på et senere tidspunkt og ikke kommer i direkte forbindelse med resten af SQL-kommandoen. Databasen tager sig selv af den fornødne escaping af parametrene, hvorved SQL-injection forhindres.
Der er derfor ikke længere brug for at 'hacke' sig frem med mysql_real_escape_string el.lign.
En anden fordel er, at planen kan genbruges, hvilket i høj grad forbedrer performance ved gentagne ens forespørgsler med forskellige værdier. Det skal nævnes, at der ved en enkelt forespørgsel er tale om en marginalt dårligere performance, hvilket dog opvejes af den dramatisk forbedrede sikkerhed.
Kodeeksempler
For at åbne en forbindelse til databasen bruges følgende kode, som i høj grad ligner det, vi allerede kender. Dog defineres databasen med det samme, når forbindelsen etableres:
$mysqli = new mysqli("server", "brugernavn", "kodeord", "databasenavn");
/* Tjek, om der opstod en fejl */
if (mysqli_connect_errno()) {
echo 'Der opstod en fejl ved forbindelsen: ' . mysqli_connect_error();
exit();
}
Eksempel på en SELECT forespørgsel, hvor id er større en værdi, hentet fra dokumentets URL:
/* Opret et prepared statement */
if ($stmt = $mysqli->prepare('SELECT `fornavn`, `efternavn` FROM `tabel` WHERE `id` > ?')) {
/* Bind parametre */
$stmt->bind_param('i', $id);
/* Sæt værdier på parametrene */
$id = $_GET['id'];
/* Eksekver forespørgslen */
$stmt->execute();
/* Bind resultatet */
$stmt->bind_result($fornavn, $efternavn);
/* Hent rækker og udskriv data */
while ($stmt->fetch()) {
echo $fornavn . ' ' . $efternavn . '<br>';
}
/* Luk statement */
$stmt->close();
} else {
/* Der er opstået en fejl */
echo 'Der opstod en fejl i erklæringen: ' . $mysqli->error;
}
NB: Læg mærke til udtrykket:
$stmt->bind_param('i', $id);I et kald til metoden bind_param består første argument altid af en streng med ligeså mange tegn, som der er efterfølgende parametre. I dette eksempel kun med ét: 'i' - men i det følgende eksempel med flere tegn. Disse tegn skal modsvare de pågældende variablers type - og kan antage følgende værdier:
*) s: string
*) i: integer
*) d: double
*) b: blob (binære data, sendt som pakker)
Eksempel på INSERT fra form med method="post":
/* Opret et prepared statement */
if ($stmt = $mysqli->prepare('INSERT INTO `tabel` (`fornavn`, `efternavn`, `stamp`) VALUES (?, ?, ?)')) {
/* Bind parametre */
$stmt->bind_param('ssi', $fornavn, $efternavn, $stamp_now);
/* Sæt værdier på parametrene */
$fornavn = $_POST['fornavn'];
$efternavn = $_POST['efternavn'];
$stamp_now = time();
/* Eksekver forespørgslen */
$stmt->execute();
/* Luk statement */
$stmt->close();
} else {
/* Der er opstået en fejl */
echo 'Der opstod en fejl i erklæringen: ' . $mysqli->error;
}
Eksempel på UPDATE fra form med method="post":
/* Opret et prepared statement */
if ($stmt = $mysqli->prepare('UPDATE `tabel` SET `fornavn`=? WHERE `id`=?')) {
/* Bind parametre */
$stmt->bind_param('si', $fornavn, $id);
/* Sæt værdier på parametrene */
$fornavn = $_POST['fornavn'];
$id = $_POST['id'];
/* Eksekver forespørgslen */
$stmt->execute();
/* Luk statement */
$stmt->close();
} else {
/* Der er opstået en fejl */
echo 'Der opstod en fejl i erklæringen: ' . $mysqli->error;
}
Vigtige overvejelser
I de viste eksempler er brugt $_GET og $_POST variabler uden forudgående validering. Det er kun gjort for overskuelighedens skyld. Man bør altid først validere input 'udefra' - altså data, som ikke er skrevet ind i koden af udvikleren selv.
Mange tænker i denne forbindelse ikke på, at en $_COOKIE variabel også kommer udefra. Den stammer som bekendt fra en tekstfil, der er gemt på brugerens PC og kan derfor meget vel være manipuleret.
Hvis du forventer en variabel skal indeholde et tal, så tjek, om variablen faktisk indeholder et tal - og tjek også, om det er indenfor forventede minimums- og maksimumsværdier. Forventer du en streng, så tjek, om det er en streng, og om den passer med det format, du forventer, osv.
OBS: Hvad udskrivning af fejlmeddelelser angår, så er de her viste eksempler kun ment som eksempler, beregnet for læreprocessen. I virkeligheden udgør de i sig selv en sikkerhedsrisiko, hvis de får lov at ligge på en offentlig tilgængelig server. Jeg vil derfor varmt anbefale, at du læser min guide om sikrere håndtering af MySQL-fejl.
Forslag til videre læsning
PHP-manualen om MySQLI
PHP-manualen om MySQLI klassens properties og metoder
PHP-manualen om statement klassens properties og metoder
PHP-manualen om result klassens properties og metoder


