Vraag Hoe kan ik SQL-injectie in PHP voorkomen?


Als gebruikersinvoer zonder aanpassing in een SQL-query wordt ingevoegd, wordt de toepassing kwetsbaar voor SQL injectie, zoals in het volgende voorbeeld:

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

Dat komt omdat de gebruiker zoiets kan invoeren value'); DROP TABLE table;--, en de vraag wordt:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

Wat kan worden gedaan om dit te voorkomen?


2781


oorsprong


antwoorden:


Gebruik voorbereide instructies en geparametriseerde vragen. Dit zijn SQL-instructies die door de databaseserver afzonderlijk van alle parameters worden verzonden en geparseerd. Op deze manier is het voor een aanvaller onmogelijk om schadelijke SQL te injecteren.

Je hebt eigenlijk twee opties om dit te bereiken:

  1. Gebruik makend van BOB (voor elke ondersteunde databasestuurprogramma):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute(array('name' => $name));
    
    foreach ($stmt as $row) {
        // do something with $row
    }
    
  2. Gebruik makend van MySQLi (voor MySQL):

    $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
    $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
    
    $stmt->execute();
    
    $result = $stmt->get_result();
    while ($row = $result->fetch_assoc()) {
        // do something with $row
    }
    

Als u verbinding maakt met een andere database dan MySQL, is er een specifieke, tweede stuurprogrammafunctie waarnaar u kunt verwijzen (bijv. pg_prepare() en pg_execute() voor PostgreSQL). PDO is de universele optie.

Correct opzetten van de verbinding

Merk op dat bij gebruik PDO om toegang te krijgen tot een MySQL-database echt voorbereide verklaringen zijn niet standaard gebruikt. Om dit te verhelpen, moet u de emulatie van voorbereide instructies uitschakelen. Een voorbeeld van het maken van een verbinding met PDO is:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

In het bovenstaande voorbeeld is de foutmodus niet strikt noodzakelijk, maar het is aan te raden om het toe te voegen. Op deze manier zal het script niet stoppen met een Fatal Error wanneer er iets misgaat. En het geeft de ontwikkelaar de kans om catch elke fout (en) die dat zijn thrown zo PDOExceptions.

Wat is verplichtis echter de eerste setAttribute() regel, die PDO vertelt om geëmuleerde voorbereide instructies uit te schakelen en te gebruiken echt voorbereide verklaringen. Dit zorgt ervoor dat de instructie en de waarden niet worden geparseerd door PHP voordat deze naar de MySQL-server wordt verzonden (waardoor een mogelijke aanvaller geen kans krijgt om kwaadwillende SQL te injecteren).

Hoewel u de. Kunt instellen charset in de opties van de constructor is het belangrijk om op te merken dat 'oudere' versies van PHP (<5.3.6) negeerde stil de charset-parameter in de DSN.

Uitleg

Wat er gebeurt, is de SQL-instructie waarnaar u verwijst prepare wordt geparseerd en gecompileerd door de databaseserver. Door parameters op te geven (a ? of een benoemde parameter zoals :name in het bovenstaande voorbeeld) vertel je de database-engine waarnaar je wilt filteren. Wanneer je belt execute, de voorbereide instructie wordt gecombineerd met de parameterwaarden die u opgeeft.

Het belangrijkste hierbij is dat de parameterwaarden worden gecombineerd met de gecompileerde instructie, niet met een SQL-tekenreeks. SQL-injectie werkt door het script om te leiden naar kwaadwillende reeksen wanneer het SQL maakt om naar de database te verzenden. Dus door de daadwerkelijke SQL apart van de parameters te verzenden, beperkt u het risico dat u eindigt met iets dat u niet van plan was. Alle parameters die u verzendt bij gebruik van een voorbereide instructie, worden gewoon als strings behandeld (hoewel de database-engine mogelijk enige optimalisatie uitvoert, dus parameters kunnen natuurlijk ook als nummers eindigen). In het bovenstaande voorbeeld, als de $namevariabele bevat 'Sarah'; DELETE FROM employees het resultaat zou simpelweg een zoekopdracht naar de string zijn "'Sarah'; DELETE FROM employees"en je komt er niet uit een lege tafel.

Een ander voordeel van het gebruik van voorbereide instructies is dat als u dezelfde instructie vele malen in dezelfde sessie uitvoert, deze slechts één keer wordt geparseerd en gecompileerd, waardoor u enige snelheidsvoordelen behaalt.

Oh, en aangezien je vroeg hoe je het moet doen voor een insert, hier is een voorbeeld (met behulp van PDO):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute(array('column' => $unsafeValue));

Kunnen voorbereide instructies worden gebruikt voor dynamische query's?

Hoewel u nog steeds voorbereide instructies voor de queryparameters kunt gebruiken, kan de structuur van de dynamische query zelf niet worden geparametriseerd en kunnen bepaalde queryfuncties niet worden geparametriseerd.

Voor deze specifieke scenario's is het het beste om een ​​whitelist-filter te gebruiken dat de mogelijke waarden beperkt.

// Value whitelist
// $dir can only be 'DESC' otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}

7938



Waarschuwing:   De voorbeeldcode van dit antwoord (zoals de voorbeeldcode van de vraag) gebruikt PHP's mysql extensie, die is beëindigd in PHP 5.5.0 en volledig is verwijderd in PHP 7.0.0.

Als u een recente versie van PHP gebruikt, is de mysql_real_escape_string de onderstaande optie is niet langer beschikbaar (hoewel mysqli::escape_string is een modern equivalent). Tegenwoordig de mysql_real_escape_string optie zou alleen zinvol zijn voor oude code op een oude versie van PHP.


Je hebt twee opties - ontsnappen aan de speciale personages in je unsafe_variableof met behulp van een geparametriseerde query. Beide zouden je beschermen tegen SQL-injectie. De geparametriseerde query wordt beschouwd als de betere oefening, maar moet worden gewijzigd in een nieuwere mysql-extensie in PHP voordat je deze kunt gebruiken.

We behandelen de onderste inslagstreng die als eerste ontsnapt.

//Connect

$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);

mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

//Disconnect

Zie ook de details van de mysql_real_escape_string functie.

Om de geparametriseerde query te gebruiken, moet u gebruiken MySQLi liever dan de MySQL functies. Om uw voorbeeld te herschrijven, hebben we zoiets als het volgende nodig.

<?php
    $mysqli = new mysqli("server", "username", "password", "database_name");

    // TODO - Check that connection was successful.

    $unsafe_variable = $_POST["user-input"];

    $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");

    // TODO check that $stmt creation succeeded

    // "s" means the database expects a string
    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();
?>

De belangrijkste functie die u daar wilt lezen zou zijn mysqli::prepare.

Ook, zoals anderen hebben gesuggereerd, zou je het misschien handig / gemakkelijker vinden om een ​​laag van abstractie met zoiets op te voeren BOB.

Houd er rekening mee dat de zaak waar u naar vroeg een vrij eenvoudige zaak is en dat voor complexere gevallen complexere benaderingen vereist kunnen zijn. Met name:

  • Als u de structuur van de SQL op basis van gebruikersinvoer wilt wijzigen, zullen geparametriseerde query's niet helpen en wordt het vereiste escaperen niet gedekt door mysql_real_escape_string. In dit soort gevallen zou het beter zijn om de invoer van de gebruiker via een whitelist door te geven om ervoor te zorgen dat alleen 'veilige' waarden worden toegestaan.
  • Als u gehele getallen van gebruikersinvoer in een voorwaarde gebruikt en de mysql_real_escape_string aanpak, zult u last hebben van het probleem dat wordt beschreven door Polynomial in de reacties hieronder. Deze case is lastiger omdat gehele getallen niet tussen aanhalingstekens staan, dus je zou kunnen afhandelen door te valideren dat de gebruikersinvoer alleen cijfers bevat.
  • Er zijn waarschijnlijk andere gevallen waar ik niet van op de hoogte ben. Misschien vindt u het deze is een nuttige bron voor enkele van de meer subtiele problemen die u kunt tegenkomen.

1509



Elk antwoord hier behandelt slechts een deel van het probleem.
In feite zijn er vier verschillende queryonderdelen die we dynamisch kunnen toevoegen:

  • Een touwtje
  • een getal
  • een identifier
  • een syntax sleutelwoord.

en voorbereide verklaringen beslaat slechts 2 van hen

Maar soms moeten we onze vraag nog dynamischer maken door operators of ID's toe te voegen.
We zullen dus verschillende beveiligingstechnieken nodig hebben.

Over het algemeen is een dergelijke beschermingsaanpak gebaseerd op whitelisting. In dit geval moet elke dynamische parameter in uw script worden gecodeerd en uit die set worden gekozen.
Bijvoorbeeld om dynamisch te bestellen:

$orders  = array("name","price","qty"); //field names
$key     = array_search($_GET['sort'],$orders)); // see if we have such a name
$orderby = $orders[$key]; //if not, first one will be set automatically. smart enuf :)
$query   = "SELECT * FROM `table` ORDER BY $orderby"; //value is safe

Er is echter nog een andere manier om ID's te beveiligen - ontsnappen. Zolang je een identifier hebt aangehaald, kun je backticks binnenin ontwijken door ze te verdubbelen.

Als een volgende stap kunnen we een echt briljant idee lenen van het gebruik van een tijdelijke aanduiding (een proxy om de werkelijke waarde in de query weer te geven) uit de voorbereide instructies en een plaatshouder van een ander type uitvinden - een tijdelijke plaatsaanduider.

Dus, om het lange verhaal kort te maken: het is een placeholder, niet voorbereide verklaring kan worden beschouwd als een zilveren kogel.

Dus een algemene aanbeveling kan als volgt worden geformuleerd
Zolang u dynamische onderdelen aan de query toevoegt met behulp van plaatsaanduidingen (en deze tijdelijke aanduidingen correct zijn verwerkt), kunt u er zeker van zijn dat uw vraag veilig is.

Toch is er een probleem met SQL syntax-sleutelwoorden (zoals AND, DESC en dergelijke) maar white-listing lijkt in dit geval de enige aanpak.

Bijwerken

Hoewel er een algemene overeenkomst is over de beste werkwijzen met betrekking tot SQL-injectiebescherming, zijn er wel degelijk nog steeds veel slechte praktijken. En sommigen van hen hebben te diep geworteld in de hoofden van PHP-gebruikers. Bijvoorbeeld, op deze zelfde pagina zijn er (hoewel onzichtbaar voor de meeste bezoekers) meer dan 80 verwijderde antwoorden - allemaal verwijderd door de gemeenschap vanwege slechte kwaliteit of het promoten van slechte en verouderde praktijken. Erger nog, sommige van de slechte antwoorden zijn niet verwijderd, maar eerder welvarend.

Bijvoorbeeld, Er (1)  zijn (2)  stil (3)  veel (4)  antwoorden (5), inclusief het tweede meest opbeide antwoord suggereert dat je handmatige string ontsnapt - een verouderde aanpak die onveilig is gebleken.

Of er is een iets beter antwoord dat juist suggereert een andere methode voor string-opmaak en biedt het zelfs als ultieme wondermiddel. Hoewel dat natuurlijk niet zo is. Deze methode is niet beter dan normale tekenreeksopmaak, maar behoudt al zijn nadelen: deze is alleen van toepassing op tekenreeksen en is, net als elke andere handmatige opmaak, in wezen een optionele, niet-verplichte maat, vatbaar voor menselijke fouten van welke aard dan ook.

Ik denk dat dit alles vanwege een heel oud bijgeloof, ondersteund door dergelijke autoriteiten, zoals OWASP of PHP-handleiding, die gelijkheid uitroept tussen alles wat "ontsnapt" en bescherming tegen SQL-injecties.

Ongeacht wat PHP-handboek eeuwenlang zei, *_escape_string maakt geen gegevens veilig en nooit is bedoeld om. Behalve dat het nutteloos is voor een ander SQL-onderdeel dan tekenreeks, is handmatige escape verkeerd, omdat het handboek is als tegengesteld aan geautomatiseerd.

En OWASP maakt het nog erger, met de nadruk op ontsnapping gebruikers invoer dat is volslagen onzin: zulke woorden zouden er niet moeten zijn in de context van injectiebescherming. Elke variabele is potentieel gevaarlijk - ongeacht de bron! Of, met andere woorden: elke variabele moet correct worden opgemaakt om in een query te worden geplaatst, ongeacht de bron. Het is de bestemming die ertoe doet. Op het moment dat een ontwikkelaar de schapen begint te scheiden van de geiten (denkend dat een bepaalde variabele "veilig" is of niet) zet hij zijn eerste stap naar een ramp. Om nog maar te zwijgen van het feit dat zelfs de bewoordingen suggereren dat bulk ontsnapt bij het instappunt, wat lijkt op de zeer magische quotes-functie - al veracht, verouderd en verwijderd.

Dus, in tegenstelling tot wat "ontsnapt", voorbereide uitspraken is de maatregel die inderdaad beschermt tegen SQL-injectie (indien van toepassing).

Als je nog steeds niet overtuigd bent, is hier een stapsgewijze uitleg die ik heb geschreven, The Hitchhiker's Guide to SQL Injection prevention, waar ik al deze zaken in detail heb uitgelegd en zelfs een sectie samengesteld die volledig is toegewijd aan slechte praktijken en de onthulling ervan.


940



Ik zou het aanraden om te gebruiken BOB (PHP Data Objects) om geparametriseerde SQL-query's uit te voeren.

Dit beschermt niet alleen tegen SQL-injectie, het versnelt ook zoekopdrachten.

En door PDO te gebruiken in plaats van mysql_, mysqli_, en pgsql_ functies, maakt u uw app een beetje meer geabstraheerd van de database, in het zeldzame geval dat u moet schakelen tussen databaseleveranciers.


768



Gebruik PDO en bereidde vragen voor.

($conn is een PDO voorwerp)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();

565



Zoals je kunt zien, stellen mensen dat je maximaal voorbereide uitspraken gebruikt. Het is niet verkeerd, maar als uw vraag wordt uitgevoerd maar een keer per proces zou er een lichte prestatieboete zijn.

Ik kreeg te maken met dit probleem, maar ik denk dat ik het heb opgelost heel geavanceerde manier - de manier waarop hackers gebruiken om citaten te vermijden. Ik gebruikte dit in combinatie met geëmuleerde voorbereide uitspraken. Ik gebruik het om te voorkomen alle soorten mogelijke SQL-injectie-aanvallen.

Mijn aanpak:

  • Als u verwacht dat de invoer een geheel getal is, zorg er dan voor dat dit het geval is werkelijk geheel getal. In een taal van het variabele type zoals PHP is dit dit heel belangrijk. U kunt bijvoorbeeld deze zeer eenvoudige maar krachtige oplossing gebruiken: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input); 

  • Als u iets anders verwacht van integer hex het. Als je het hex, zal je perfect ontsnappen aan alle invoer. In C / C ++ is er een functie genaamd mysql_hex_string(), in PHP kunt u gebruiken bin2hex().

    Maak je geen zorgen dat de ontsnapte string een 2x-formaat van de oorspronkelijke lengte heeft, zelfs als je deze gebruikt mysql_real_escape_string, PHP moet dezelfde capaciteit toewijzen ((2*input_length)+1), wat hetzelfde is.

  • Deze hex-methode wordt vaak gebruikt wanneer u binaire gegevens overdraagt, maar ik zie geen reden waarom deze niet op alle gegevens te gebruiken om SQL-injectieaanvallen te voorkomen. Houd er rekening mee dat u gegevens moet toevoegen met 0x of gebruik de MySQL-functie UNHEX in plaats daarvan.

Dus, bijvoorbeeld, de vraag:

SELECT password FROM users WHERE name = 'root'

Zal worden:

SELECT password FROM users WHERE name = 0x726f6f74

of

SELECT password FROM users WHERE name = UNHEX('726f6f74')

Hex is de perfecte ontsnapping. Geen manier om te injecteren.

Verschil tussen UNHEX-functie en 0x-voorvoegsel

Er was wat discussie in opmerkingen, dus ik wil het eindelijk duidelijk maken. Deze twee benaderingen lijken erg op elkaar, maar ze verschillen op een bepaalde manier enigszins:

Het voorvoegsel ** 0x ** kan alleen worden gebruikt voor gegevenskolommen zoals char, varchar, text, block, binary, etc..
Ook is het gebruik ervan een beetje ingewikkeld als je op het punt staat een lege string in te voegen. Je zult het volledig moeten vervangen '', anders krijg je een foutmelding.

UNHEX () werkt aan ieder kolom; je hoeft je geen zorgen te maken over de lege string.


Hex-methoden worden vaak gebruikt als aanvallen

Merk op dat deze hex-methode vaak wordt gebruikt als een SQL-injectieaanval, waarbij gehele getallen net als strings zijn en net zijn geëscaped mysql_real_escape_string. Dan kunt u het gebruik van citaten vermijden.

Als u bijvoorbeeld zoiets als dit doet:

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

een aanval kan je heel veel injecteren gemakkelijk. Beschouw de volgende ingespoten code die is geretourneerd uit uw script:

SELECTEER ... WAAR id = -1 union all selecteer table_name uit information_schema.tables

en nu gewoon de tabelstructuur extraheren:

SELECT ... WHERE id = -1 union selecteer kolomnaam uit information_schema.column waarbij table_name = 0x61727469636c65

En selecteer vervolgens welke gegevens u maar wilt. Is het niet cool?

Maar als de codeur van een injecteerbare site het zou invullen, zou geen injectie mogelijk zijn omdat de zoekopdracht er zo zou uitzien: SELECT ... WHERE id = UNHEX('2d312075...3635')


498



BELANGRIJK

De beste manier om SQL-injectie te voorkomen, is om te gebruiken Voorbereide verklaringen  in plaats van te ontsnappen, als het geaccepteerde antwoord aantoont.

Er zijn bibliotheken zoals Aura.Sql en EasyDB waarmee ontwikkelaars voorbereide instructies gemakkelijker kunnen gebruiken. Voor meer informatie over waarom voorbereide beweringen beter zijn stoppen met SQL-injectie, verwijzen naar deze mysql_real_escape_string() bypass en onlangs opgeloste Unicode SQL Injection-kwetsbaarheden in WordPress.

Injectiepreventie - mysql_real_escape_string ()

PHP heeft een speciaal gemaakte functie om deze aanvallen te voorkomen. Het enige wat je hoeft te doen is de mondvol van een functie gebruiken, mysql_real_escape_string.

mysql_real_escape_string neemt een string die gebruikt gaat worden in een MySQL-query en retourneert dezelfde string met alle SQL-injectiepogingen veilig ontsnapt. In principe zal het die lastige aanhalingstekens (') vervangen die een gebruiker kan invoeren met een MySQL-veilig alternatief, een ontsnapt citaat \'.

NOTITIE: je moet verbonden zijn met de database om deze functie te gebruiken!

// Maak verbinding met MySQL

$name_bad = "' OR 1'"; 

$name_bad = mysql_real_escape_string($name_bad);

$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";


$name_evil = "'; DELETE FROM customers WHERE 1 or username = '"; 

$name_evil = mysql_real_escape_string($name_evil);

$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;

U kunt meer informatie vinden in MySQL - Preventie van SQL-injectie.


452



Veiligheidswaarschuwing: Dit antwoord is niet in overeenstemming met de beste beveiligingsmethoden. Ontsnappen is onvoldoende om SQL-injectie te voorkomen, gebruik voorbereide verklaringen in plaats daarvan. Gebruik de onderstaande strategie op eigen risico. (Ook, mysql_real_escape_string() is verwijderd in PHP 7.)

Je zou zoiets basic kunnen doen als dit:

$safe_variable = mysql_real_escape_string($_POST["user-input"]);
mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

Dit lost niet elk probleem op, maar het is een zeer goede opstap. Ik liet voor de hand liggende items weg, zoals het controleren van het bestaan ​​van de variabele, het formaat (cijfers, letters, etc.).


410



Wat je uiteindelijk ook gebruikt, zorg ervoor dat je controleert of je invoer nog niet is verminkt magic_quotes of een andere goedbedoelde onzin, en indien nodig, maak het door stripslashes of wat dan ook om het te zuiveren.


345