Imprimir

Manual de PHP: Sanear datos de usuario

Por qué tenemos que sanear los datos de usuario (sanitize user inputs)

En las aplicaciones web, por defecto, debemos suponer que los datos procedentes de los usuarios no son seguros. Al contrario, hay muchos usuarios malintencionados que tratan de explotar las vulnerabilidades de seguridad de nuestra aplicación y los ataques XSS, CSRF o SQL Injection son familiares para todos los webmaster.

Con el fin de proteger la aplicación de tales ataques es necesario sanear los datos de usuario ("sanitize user inputs") de modo que no dañen nuestro sistema, entendiendo por "datos de usuario" no sólo los datos suministrados por los usuarios (búsquedas, formularios, cookies, referers, etc.) sino también los procedentes de nuestra base de datos, de terceras partes (como servicios web, feeds, etc.) y en general cualquier dato cuyo origen no sea absolutamente fiable.

Exploits of a Mom

Cómo sanear los datos de usuario

Existen varias soluciones para resolver el problema de cómo sanear los datos de usuario pero, en opinión de muchos expertos, el sistema más recomendable es hacer un doble saneamiento:
- una Input Validation y SQL Encoding antes de almacenar los inputs en la base de datos.
- un Output Filtering antes de enviar los datos a la salida.

Siempre es mejor tener datos inalterados en la base de datos, ya que nunca se sabe cómo van a ser utilizados o si otras aplicaciones van a utilizar nuestra base de datos. Por eso es preferible sanear los datos en la fase del output, ya que si los saneamos antes de almacenarlos en la base de datos una parte de la información se perderá sin posibilidad de recuperarla y ya no será posible obtener los datos originales. Por ejemplo, es preferible guardar en la base de datos:

Hello <strong>World</strong>

que:

Hello &lt;strong&gt;World&lt;/strong&gt;

Otro ejemplo. Si un usuario envía el comentario:

More <b>info<b> en <a href="http://www.domain.com/sample.php">sample</a>

y saneamos la entrada antes de guardar en la base de datos con:

<?php
// si los datos no deben contener tags HTML o solo algunos, filtrar
$output = strip_tags($input, '<b><i><img>');
?>

la cadena que almacenaremos en la base de datos será:

More <b>info<b> en sample

por lo que habremos perdido parte de la entrada del usuario de manera irreversible. Si más adelante decidimos permitir la etiqueta <a> ya no podremos recuperar esa información, cosa que sí podremos hacer si saneamos en el output.

Por otra parte, si saneamos la entrada antes de guardarla en la base de datos y existe algún bug en nuestro script saneador, no sólo tendremos que corregir el bug sino que además tendremos que eliminar los datos maliciosos de nuestra base de datos, cosa sumamente problemática. Por el contrario, saneando los datos en la fase de output sólo tendremos que corregir el bug para solventar el problema.

Cómo sanear los datos de usuario en la práctica

En la práctica, este sistema de doble saneamiento lo haremos en tres pasos:

  1. Input Validation

    Lo más recomendable es limitarse a validar los datos, sin intentar corregirlos. Si validan se aceptan, si no validan los rechazaremos. Intentar alterarlos para insertarlos en la base de datos generalmente sólo provoca problemas. En los inputs comprobaremos varias cosas:

    • Codificación de caracteres

      Comprobaremos si la codificación de caracteres es la adecuada (UTF-8).

    • Caracteres

      Nos aseguraremos de que los datos sólo contienen caracteres permitidos.

    • Tipo de datos

      Comprobaremos que el tipo de dato es el esperado: los teléfonos deben contener sólo números, etc.

    • Longitud (min/max)

      Es recomendable restringir la longitud de los input, ya que así limitamos las opciones de los atacantes.

    • Formato de los datos

      Comprobaremos que la estructura de los datos es consistente con lo esperado: los teléfonos deben parecer teléfonos, los email deben parecer email, etc.

  2. SQL Encoding

    Si los datos validan, escaparemos para SQL y los almacenaremos en la base de datos (usaremos la función mysql_real_escape_string( ) de PHP).

    <?php
    // connect
    $link = mysql_connect('mysql_host', 'mysql_user', 'mysql_password')
        OR die(mysql_error());
     
    // escapar los caracteres que no son legales en SQL
    // requiere una conexion MySQL, de lo contrario dara error
    $user     = mysql_real_escape_string(trim($user));
    $password = mysql_real_escape_string(trim($password));
     
    // query
    $query = "INSERT INTO users (user, password) VALUES ('$user', '$password')";
    ?>
  3. Output Filtering

    En función del destino de los datos aplicaremos diferentes filtros antes del output.

    • HTML Encoding

      Si los datos van a una página web los escaparemos para HTML (usaremos la función htmlentities( ) de PHP).

      <?php
      $str = "A 'quote' is <b>bold</b>";
      echo htmlentities(trim($str), ENT_QUOTES);
      ?>

      El output será:

      A &#039;quote&#039; is &lt;b&gt;bold&lt;/b&gt;
    • Shell Encoding

      Si los datos van a la línea de comandos como argumento, los escaparemos para "shell argument" (usaremos la función escapeshellarg( ) de PHP).

      <?php
      $argument = escapeshellarg(trim($argument));
      system('ls '.$argument);
      ?>

      Si los datos van a la línea de comandos como un comando, los escaparemos para "shell command" (usaremos la función escapeshellcmd( ) de PHP).

      <?php
      $command = escapeshellcmd(trim($command));
      system("echo $command");
      ?>
    • URL Encoding

      Si los datos van a formar parte de una URL, los escaparemos para URL usando las funciones urlencode( ) y htmlentities( ) de PHP (htmlentities traduce a HTML el separador & y lo convierte en &amp;).

      <?php
      $query_string = '?foo=' . $foo . '&bar=' . $bar;
      $url = 'http://www.domain.com/mycgi' . $query_string;
      $url = htmlentities(urlencode(trim($url)));
      echo '<a href="'. $url . '">link</a>';
      ?>
    • Codificación de caracteres del output

      Nos aseguraremos de que el browser aplica la codificación de caracteres adecuada especificando el charset (UTF-8). Disponemos para ello de dos métodos.

      - Response Header:

      Content-Type: text/html; charset=utf-8

      - Meta Tags:

      <meta http-equiv="Content-Type" content="text/html; charset= utf-8" />

Ejemplo de función que sanea los datos de usuario

Vamos a ver un ejemplo de función que sanea los datos de usuario aplicando el sistema de doble saneamiento que acabamos de ver, la función Sanitize():

<?php
/*
  Sanitize function
  Devuelve la variable saneada o TRUE/FALSE si la variable valida/no valida
  $input: variable a sanear
  $type:  lo que queremos comprobar
*/
function Sanitize($input, $type) {
  switch ($type) {
    // 1- Input Validation
 
    case 'int': // comprueba si $input es integer
      if (is_int($input)) {
        $output = TRUE;
      } else {
        $output = FALSE;
      }
      break;
 
    case 'str': // comprueba si $input es string
      if (is_string($input)) {
        $output = TRUE;
      } else {
        $output = FALSE;
      }
      break;
 
    case 'digit': // comprueba si $input contiene solo numeros
      if (ctype_digit($input)) {
        $output = TRUE;
      } else {
        $output = FALSE;
      }
      break;
 
    case 'upper': // comprueba si $input contiene solo mayusculas
      if ($input == strtoupper($input)) {
        $output = TRUE;
      } else {
        $output = FALSE;
      }
      break;
 
    case 'lower': // comprueba si $input contiene solo minusculas
      if ($input == strtolower($input)) {
        $output = TRUE;
      } else {
        $output = FALSE;
      }
      break;
 
    case 'mobile': // comprueba si $input contiene 9 numeros
      if ((strlen($input) == 9) && (ctype_digit($input) == TRUE)) {
        $output = TRUE;
      } else {
        $output = FALSE;
      }
      break;
 
    case 'email': // comprueba si $input tiene formato de email
      $reg_exp = "/^[-\.0-9A-Z]+@([-0-9A-Z]+\.)+([0-9A-Z]){2,4}$/i";
      if (preg_match($reg_exp, $input)) {
        $output = TRUE;
      } else {
        $output = FALSE;
      }
      break;
 
    // 2- SQL Encoding
 
    case 'sql': // escapar los caracteres que no son legales en SQL
 
      // si magic_quotes_gpc esta activado primero aplicar stripslashes()
      // de lo contrario los datos seran escapados dos veces
      if (get_magic_quotes_gpc()) {
        $input = stripslashes($input);
      }
 
      // requiere una conexion MySQL, de lo contrario dara error
      $output = mysql_real_escape_string(trim($input));
      break;
 
    // 3- Output Filtering
 
    case 'no_html': // los datos van a una pagina web, escapar para HTML
      $output = htmlentities(trim($input), ENT_QUOTES);
      break;
 
    case 'shell_arg': // los datos van al shell, escapar para shell argument
      $output = escapeshellarg(trim($input));
      break;
 
    case 'shell_cmd': // los datos van al shell, escapar para shell command
      $output = escapeshellcmd(trim($input));
      break;
 
    case 'url': // los datos forman parte de una URL, escapar para URL
 
      // htmlentities() traduce a HTML el separador &
      $output = htmlentities(urlencode(trim($input)));
      break;
 
    case 'comment': // los datos solo pueden contener algunos tags HTML
      $output = strip_tags($input, '<b><i><img>');
      break;
  }
  return $output;
}
?>

Veamos cómo usar la función Sanitize(). Supongamos que hemos recibido una variable procedente de un formulario. Para sanear los datos, primero Input Validation y SQL Encoding: comprobaremos si validan (en este ejemplo ponemos como condición que la variable sea un string) y si es así, los guardaremos en la base de datos:

<?php
// si valida, escapar para SQL y guardar
if (Sanitize($input, 'str')) {
    // connect
    $link = mysql_connect('mysql_host', 'mysql_user', 'mysql_password')
        OR die(mysql_error());
 
    // escapar para SQL
    // requiere una conexion MySQL, de lo contrario dara error
    $input = Sanitize($input, 'sql');
 
    // query
    $query = "INSERT INTO comments (comment) VALUES ('$input')";
}
?>

Para utilizar esos datos, Output Filtering: supongamos que se trata de un comentario de un blog. Aplicaremos dos filtros, por una parte, los datos no deben contener tags HTML (excepto los permitidos, <b><i><img>), por otra parte, como los datos van a una página web, los escaparemos para HTML:

<?php
// los datos no deben contener tags HTML, excepto <b><i><img>
$output = Sanitize($output, 'comment');
 
// los datos van a una pagina web, escapar para HTML
$output = Sanitize($output, 'no_html');
echo $output;
?>

4 Comentarios en “Manual de PHP: Sanear datos de usuario”

  • hola nesecito validar un formulario en donde las cajas de texto en donde uno ingrese la cedula acepte solo numero al igual que en donde quiera ingresar letra sea solo letras al igual que en el imail aqui le envio el fomulario o los codigo en php espero me ayuden lo mas antes posible gracias

    "Liceo Bolivariano Humboldt"1){
    echo “Ya Este Numero de Cedula existe”;
    }else{
    $checkemail = mysql_query(”SELECT email FROM adscrito WHERE email=’$email’”);
    $email_exist = mysql_num_rows($checkemail);

    if ($email_exist>0) {
    echo “Esta cuenta de correo electronico estan ya en uso”;
    }else{
    $insertar=”insert into adscrito values (’$cedula’, ‘$nombre’, ‘$apellido’, ‘$edad’, ‘$direccion’, ‘$fecha’, ‘$telefono’, ‘$sexo’, ‘$anio’, ‘$mencion’, ‘$email’, ‘$tipo’, ‘$institucion’)”;
    $result=mysql_query($insertar);
    if($result){
    echo “La Insercion es exitosa”;
    }else{
    echo “error”;
    }
    ?>

    me envian la ayuda a mi correo si es posible

  • Edith dice:

    Excelente artículo! Muchas gracias (:

  • Nil dice:

    Muy buen articulo, te felicito! He aprendido bastantes cosas : D

  • Adrian dice:

    Hola, buen artículo. Quisiera saber donde puedo encontrar una lista de mas funciones de limpieza de codigo PHP, ademas de funciones que logran lo contrario. Saludos.

Deja un comentario