Recogida de datos

En esta lección se comentan algunos aspectos prácticos que debemos tener en cuenta en nuestros programas al recoger datos de formularios (entre ellos, el más importante, la seguridad ante ataques). En los ejercicios propuestos en estos apuntes (sección PHP, con formularios) se espera que se apliquen esas recomendaciones. Para facilitar su aplicación, se recomienda el uso de la función que se comenta en la lección Función recoge(). También se recomienda implementar la comprobación de los datos recibidos, como se comenta en las lecciones Funciones de comprobación de datos y Ejemplos de comprobación de datos.

¡Atención! Algunos ejemplos de esta lección describen programas que pueden generar mensajes de error o ilustran un valor de entrada concreto en el formulario. Por motivos de seguridad, esos programas no están operativos y en los ejemplos los botones Enviar están desactivados. El resto de programas de los ejemplos sí están operativos y se pueden probar y abrir en pestañas separadas.

Matriz $_REQUEST

Cuando se envía un formulario, PHP almacena la información recibida en una matriz llamada $_REQUEST. El número de valores recibidos y los valores recibidos dependen tanto del formulario como de la acción del usuario.

Cualquier control se envía solamente si está establecido su atributo name. El atributo name del control puede contener cualquier carácter (números, acentos, guiones, etc), pero si contiene espacios, los espacios se sustituyen por guiones bajos (_). Cada control crea un elemento de la matriz $_REQUEST, que se identifica como $_REQUEST[valor_del_atributo_name] y que contiene el valor entregado por el formulario (en su caso).

El siguiente ejemplo muestra el aspecto de un formulario con una caja de texto y un botón Enviar (aunque este ejemplo no envía el dato a ningún sitio y la caja de texto no permite escribir en ella):

ejemplo-1.php (código HTML)
ejemplo-1.php (visualización del formulario NO operativo)
<form action="ejemplo.php">
  <p>Nombre: <input type="text" name="nombre"></p>
  <p><input type="submit" value="Enviar"></p>
</form>

Nombre:

En los ejemplos de esta lección se utiliza la función print_r($matriz) para mostrar el contenido de la matriz $_REQUEST y así comprobar la información enviada por el control. Se han añadido etiquetas <pre> alrededor del print_r($_REQUEST) para facilitar la visualización de los valores. Esto se puede hacer cuando se está programando y probando los programas, pero una vez comprobado que la información llega correctamente, se recomienda eliminar este fragmento.

El siguiente ejemplo muestra un programa PHP que podría recibir la información del formulario anterior. Como este programa incluye sin modificaciones el texto recibido, en algunos casos el resultado puede ser problemático, como se comenta en apartados posteriores de esta lección.

ejemplo-1.php (formulario)
ejemplo-2.php (código fuente)
ejemplo-2.php (resultado)
<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

print "  <p>Su nombre es $_REQUEST[nombre]</p>\n";
?>
Array
(
   [nombre] => Pepito Conejo
)

Su nombre es Pepito Conejo

Enlace a ejemplo

Referencia a $_REQUEST dentro y fuera de cadenas

Al hacer referencia a los elementos de la matriz $_REQUEST, como con cualqueir otra matriz hay que tener en cuenta si la referencia se encuentra dentro de una cadena o fuera de ella.

En ningún caso se pueden utilizar los caracteres especiales \" o \', ni dentro ni fuera de las cadenas.

Incorrecto
<?php
print "  <p>Su nombre es $_REQUEST[\"nombre\"]</p>\n";
?>
Parse error: syntax error, unexpected '' (T_ENCAPSED_AND_WHITESPACE), expecting identifier (T_STRING) or variable (T_VARIABLE) or number (T_NUM_STRING) in ejemplo.php on line 2
Incorrecto
<?php
print "  <p>Su nombre es " . $_REQUEST[\'nombre\'] . "</p>\n";
?>
Parse error: syntax error, unexpected '' (T_ENCAPSED_AND_WHITESPACE), expecting identifier (T_STRING) or variable (T_VARIABLE) or number (T_NUM_STRING) in ejemplo.php on line 2

Comprobación de existencia

Un programa PHP que recibe datos provinientes de un formulario no debe suponer que los datos o las referencias a los controles le van a llegar siempre, porque cuando eso no ocurra el programa no funcionará correctamente.

Controles vacíos

La primera situación que los programas deben tener en cuenta es que lleguen las referencias a los controles, pero sin datos.

En el programa de ejemplo del principio de esta lección, si el usuario ha utilizado el formulario y ha escrito un dato, el programa PHP recibe el control y funciona correctamente:

ejemplo-1.php (formulario NO operativo)
ejemplo-2.php (código fuente)
ejemplo-2.php (resultado)

Nombre:

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

print "  <p>Su nombre es $_REQUEST[nombre]</p>\n";
?>
Array
(
    [nombre] => Pepito Conejo
)

Su nombre es Pepito Conejo

Pero si el usuario no escribe nada en el formulario, aunque el programa funcione correctamente, la respuesta del programa puede confundir al usuario:

ejemplo-1.php (formulario NO operativo)
ejemplo-2.php (código fuente)
ejemplo-2.php (resultado)

Nombre:

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

print "  <p>Su nombre es $_REQUEST[nombre]</p>\n";
?>
Array
(
    [nombre] =>
)

Su nombre es

Esta situación se puede resolver incluyendo una estructura if ... else ... que considere la posibilidad de que no se haya escrito nada en el formulario:

ejemplo-1.php (formulario)
ejemplo-2.php (código fuente)
ejemplo-2.php (resultado)
<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

if ($_REQUEST["nombre"] == "") {
    print "  <p>No ha escrito ningún nombre</p>";
} else {
    print "  <p>Su nombre es $_REQUEST[nombre]</p>\n";
}
?>
Array
(
    [nombre] =>
)

No ha escrito ningún nombre

Array
(
    [nombre] => Pepito Conejo
)

Su nombre es Pepito Conejo

Enlace a ejemplo

Controles inexistentes: isset()

Un problema más grave es que el programa suponga que le ha llegado un control que en realidad no le ha llegado. Eso ocurre, por ejemplo, en el caso de las casillas de verificación y los botones radio, ya que los formularios envían estos controles solamente cuando se han marcado, pero puede ocurrir también con cualquier control.

El siguiente ejemplo muestra el aspecto de un formulario con una casilla de verificación y un botón Enviar (aunque este ejemplo no envía el dato a ningún sitio):

ejemplo-1.php (código HTML)
ejemplo-1.php (visualización del formulario NO operativo)
<form action="ejemplo.php">
  <p>Deseo recibir información: <input type="checkbox" name="acepto"></p>
  <p><input type="submit" value="Enviar"></p>
</form>

Deseo recibir información:

El siguiente ejemplo muestra un programa PHP similar al programa del apartado anterior que incluía una estructura if ... else ... para tener en cuenta que el dato puede llegar o no llegar. Si en el formulario anterior el usuario marca la casilla, el programa funciona perfectamente:

ejemplo-1.php (formulario NO operativo)
ejemplo-2.php (código fuente)
ejemplo-2.php (resultado)

Deseo recibir información:

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

if ($_REQUEST["acepto"] == "on") {
    print "  <p>Desea recibir información</p>\n";
} else {
    print "  <p>No desea recibir información</p>\n";
}
?>
Array
(
    [acepto] => on
)

Desea recibir información

Pero si el usuario no marca la casilla, la matriz $_REQUEST no contiene el dato y el programa genera un aviso por hacer referencia a un índice no definido, aunque se haya incluido la estructura if ... else ... :

ejemplo-1.php (formulario NO operativo)
ejemplo-2.php (código fuente)
ejemplo-2.php (resultado)

Deseo recibir información:

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

if ($_REQUEST["acepto"] == "on") {
    print "  <p>Desea recibir información</p>\n";
} else {
    print "  <p>No desea recibir información</p>\n";
}
?>
Array
(
)

Warning: Undefined array key "acepto" in ejemplo.php on line 4

No desea recibir información

Esto puede ocurrir también con cualquier tipo de control (incluso con cajas de texto que en principio envían siempre el control) ya que un usuario malintencionado puede escribir directamente la dirección del programa PHP en el navegador, sin pasar por el formulario y sin incluir los controles. En ese caso, el programa no recibe ningún control y el programa genera un aviso por hacer referencia a un índice no definido.


Estos problemas se pueden resolver comprobando que el índice está definido antes de hacer referencia a él, utilizando la función isset($variable), que admite como argumento una variable y devuelve true si existe y false si no existe.

ejemplo-1.php (formulario)
ejemplo-2.php (código fuente)
ejemplo-2.php (resultado)
<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

if (isset($_REQUEST["acepto"])) {
    print "  <p>Desea recibir información</p>\n";
} else {
    print "  <p>No desea recibir información</p>\n";
}
?>
Array
(
    [acepto] => on
)

Desea recibir información

Array
(
)

No desea recibir información

Enlace a ejemplo

Es conveniente efectuar siempre la verificación de existencia para prevenir los casos en que un usuario intente acceder a la página PHP sin pasar por el formulario.

Seguridad en las entradas

Otro aspecto problemático a considerar es que un usuario malintencionado puede insertar código html en la entrada de un formulario, lo que puede acarrear comportamientos inesperados y riesgos de seguridad.

En el siguiente ejemplo, el código html insertado incluye un enlace a otra página web que acaba incorporado en la página final:

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

print "  <p>Su nombre es $_REQUEST[nombre]</p>\n";
?>
Array
(
    [nombre] => <a href='https://mclibre.org' target='blank'>Pepito Conejo</a>
)
</pre>

  <p>Su nombre es <a href='https://mclibre.org' target='blank'>Pepito Conejo</a></p>

Enlace a ejemplo

El siguiente ejemplo muestra cómo puede utilizarse un formulario para inyectar en la página código JavaScript. En este caso el código genera una ventana emergente que sólo afecta al propio usuario, pero esta vía podría utilizarse para atacar al servidor.

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

print "  <p>Su nombre es $_REQUEST[nombre]</p>\n";
?>

Recogida de datos. Inyección de JavaScript

Enlace a ejemplo

Estos dos ejemplos no pondrían el riesgo el servidor, pero si en una aplicación web los datos que envía el usuario se incluyen sin precaución en una consulta SQL estaríamos hablando de una vía de ataque al servidor muy peligrosa (puede consultarse una introducción elemental a estas técnicas en la lección Inyección SQL).

Eliminar etiquetas: strip_tags($cadena)

Una manera de resolver el problema anterior es utilizar la función strip_tags($cadena), que devuelve la cadena sin etiquetas, como muestra el siguiente ejemplo.

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

print "  <p>Su nombre es $_REQUEST[nombre]</p>\n";
print "  <p>Su nombre es " . strip_tags($_REQUEST["nombre"]) . "</p>\n";
?>
<pre>Array
(
    [nombre] => <strong>Pepito Conejo</strong>
)
</pre>

<p>Su nombre es <strong>Pepito Conejo</strong></p>
<p>Su nombre es Pepito Conejo</p>
Enlace a ejemplo

Aunque hay que tener en cuenta que esta función elimina cualquier cosa que se interprete como una etiqueta, es decir, que empiece por "<" (sin espacio a continuación).

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

print "  <p>Su nombre es $_REQUEST[nombre]</p>\n";
print "  <p>Su nombre es " . strip_tags($_REQUEST["nombre"]) . "</p>\n";
?>
  <pre>
Array
(
    [nombre] => <i>bbb</i> aaa <pepe>Pérez < pepa>Pérez <Pepito Conejo
)
</pre>

  <p>Su nombre es <i>bbb</i> aaa <pepe>Pérez < pepa>Pérez <Pepito Conejo</p>

  <p>Su nombre es bbb aaa Pérez < pepa>Pérez </p>

Enlace a ejemplo

Eliminar espacios en blanco iniciales y finales: trim($cadena)

A veces, al rellenar un formulario, los usuarios escriben o pegan por error espacios en blanco (o incluso tabuladores o saltos de línea) al principio o al final de las cajas de texto y si el programa PHP no tiene en cuenta esa posibilidad se pueden obtener resultados inesperados.

En el programa de ejemplo del apartado anterior en el que se escribía una estructura if ... else ... para avisar al usuario si no escribía el nombre, si el usuario escribe únicamente espacios en blanco, el programa funciona, pero no da la respuesta adecuada ya que la cadena con espacios en blanco no es una cadena vacía.

ejemplo-1.php (formulario NO operativo)
ejemplo-2.php (código fuente)
ejemplo-2.php (resultado)

Nombre:

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

if ($_REQUEST["nombre"] == "") {
    print "  <p>No ha escrito ningún nombre</p>";
} else {
    print "  <p>Su nombre es $_REQUEST[nombre]</p>\n";
}
?>
<pre>Array
(
    [nombre] =>    
)
</pre>
<p>Su nombre es    </p>

Este problema se puede resolver utilizando la función trim($cadena), que elimina los espacios en blanco iniciales y finales (además de saltos de línea y tabuladores) y devuelve la cadena sin esos espacios. Modificando el ejemplo anterior, la cadena introducida queda reducida a la cadena vacía y la comprobación la detecta:

ejemplo-1.php (formulario)
ejemplo-2.php (código fuente)
ejemplo-2.php (resultado)
<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";
if (trim($_REQUEST["nombre"]) == "") {
    print "  <p>No ha escrito ningún nombre</p>\n";
} else {
    print "  <p>Su nombre es ". trim($_REQUEST["nombre"]) . "</p>\n";
}
?>
Array
(
    [nombre] =>
)

No ha escrito ningún nombre

Array
(
    [nombre] =>     pepe
)

Su nombre es pepe

Enlace a ejemplo

Salida de datos

Si, como en los ejemplos anteriores, los datos recogidos se van a incorporar luego a una página web, hay que tener cuidado en algunos casos especiales. Se comentan a continuación dos de ellos y después se comenta una solución general para este tipo de caracteres.

El carácter & (ampersand)

Si el usuario escribe en una entrada el carácter & (ampersand, entidad de carácter &amp;) y esa cadena se escribe en una página, la página se verá correctamente en el navegador, pero la página no será válida (el html será inválido), o la página no se verá como se espera, aunque la página sea válida.

En el ejemplo siguiente, aunque la página se visualice correctamente, el código HTML de la página es inválido porque el carácter & indica el comienzo de una entidad de carácter, así que no el código fuente no puede haber un & sin nada a continuación. Si se quiere mostrar un carácter & en el navegador, en el código fuente debe escribirse la entidad de carácter &amp;.

Incorrecto

Nombre:

<?php
if ($_REQUEST["nombre"] == "") {
    print "  <p>No ha escrito ningún nombre</p>\n";
} else {
    print "  <p>Su nombre es $_REQUEST[nombre]</p>\n";
}
?>
<p>Su nombre es Pepito & Company</p>

En el ejemplo siguiente, la página de salida del ejemplo siguiente no es inválida, pero no se muestra como se espera. Lo que ocurre es que el navegador interpreta el carácter & como el principio de una entidad de carácter, que en este caso casualmente existe (la entidad de carácter correspondiente a la letra griega micro &micro µ).

Incorrecto

Nombre:

<?php
if ($_REQUEST["nombre"] == "") {
    print "  <p>No ha escrito ningún nombre</p>\n";
} else {
    print "  <p>Su nombre es $_REQUEST[nombre]</p>\n";
}
?>

Su nombre es intelµsoft

  <p>Su nombre es intel&microsoft</p>

La solución es sustituir el carácter & por su entidad de carácter correspondiente (&amp;). Eso se puede hacer con la función str_replace()

Correcto
<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

if ($_REQUEST["nombre"] == "") {
    print "  <p>No ha escrito ningún nombre</p>\n";
} else {
    print "  <p>Su nombre es " . str_replace('&', '&amp;', $_REQUEST["nombre"]) . "</p>\n";
}
?>

Su nombre es intel&microsoft

  <pre>
Array
(
    [nombre] => intel&microsoft
)
</pre>

  <p>Su nombre es intel&amp;microsoft</p>
Enlace a ejemplo

El carácter " (comillas)

Si el usuario escribe en una entrada el carácter " (comillas, entidad de carácter &quot;), si esa cadena se escribe dentro de otras comillas (por ejemplo, en el atributo value de una etiqueta input), la página no se verá correctamente y además no será válida.

Incorrecto

Nombre:

<?php
if ($_REQUEST["nombre"] == "") {
    print "  <p>No ha escrito ningún nombre</p>\n";
} else {
    print "  <p>Corrija: <input type=\"text\" value=\"$_REQUEST[nombre]\"></p>\n";
}
?>

Corrija:

<p>Corrija: <input type="text" value="Me llamo "Pepe""></p>

El problema es que el código fuente contiene comillas dentro de comillas y el navegador no puede interpretar correctamente el código (en este caso, el valor del atributo value se trunca en "Me llamo "):

<p>Corrija: <input type="text" value="Me llamo "Pepe""></p>

Como en el caso anterior, la solución es sustituir el carácter " por su entidad de carácter correspondiente (&quot;). Eso se puede hacer con la función str_replace()

Correcto
<?php
if ($_REQUEST["nombre"] == "") {
    print "  <p>No ha escrito ningún nombre</p>\n";
} else {
    print "  <p>Corrija: <input type=\"text\" value=\"" . str_replace('"', '&quot;', $_REQUEST["nombre"]) . "\"></p>\n";
}
?>

Corrija:

  <pre>
Array
(
    [nombre] => Me llamo "Pepe"
)
</pre>

  <p>Corrija: <input type="text" value="Me llamo &quot;Pepe&quot;"></p>
Enlace a ejemplo

Solución general: htmlspecialchars()

Además de los caracteres ampersand (&) y comillas ("), también podrían dar problemas los caracteres comillas simples (') o las desigualdades (< y >). Se podría sustituir cada uno de ellos como se ha hecho en los ejemplos anteriores, pero la función htmlspecialchars() realiza todas las sustituciones de una sola vez.

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";

if ($_REQUEST["nombre"] == "") {
    print "  <p>No ha escrito ningún nombre</p>\n";
} else {
    print "  <p>Corrija: <input type=\"text\" value=\"" . htmlspecialchars($_REQUEST["nombre"]) . "\" size=\"35\"></p>\n";
}
?>

Corrija:

Enlace a ejemplo

Utilización de variables

Hemos visto que para resolver los problemas comentados en los apartados anteriores, al incluir cualquier referencia a $_REQUEST[control] habría que:

Una manera de hacerlo sin complicar excesivamente el programa es guardar los valores de la matriz $_REQUEST en variables y realizar todas las comprobaciones al definir esas variables. En el resto del código basta con utilizar la variable en vez del elemento de la matriz $_REQUEST.

En el ejemplo siguiente, se puede ver cómo el programa no genera avisos y muestra el mensaje correspondiente al dato recibido:

ejemplo-1.php (formulario)
ejemplo-2.php (código fuente)
ejemplo-2.php (resultado)
<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";
if (isset($_REQUEST["nombre"])) {
    $nombre = trim(htmlspecialchars(strip_tags($_REQUEST["nombre"])));
} else {
    $nombre = "";
}

if ($nombre == "") {
    print "  <p>No ha escrito ningún nombre</p>\n";
} else {
    print "  <p>Su nombre es $nombre</p>\n";
}
?>
<pre>Array
(
    [nombre] =>
)
</pre>

  <p>No ha escrito ningún nombre</p>
  <pre>
Array
(
    [nombre] =>      <strong>intel&microsoft</strong>
)
</pre>

  <p>Su nombre es intel&amp;microsoft</p>
Array
(
    [nombre] =>      intelµsoft
)

Su nombre es intel&microsoft

Enlace a ejemplo

En el ejemplo anterior la primera función que se aplica es strip_tags() por lo que se eliminan las etiquetas HTML. Si aplicaramos primero la función htmlspecialchars(), la función strip_tags() ya no eliminaría las etiquetas puesto que los caracteres de marcas < y > se habrán sustituido antes por las entidades de carácter &:lt; y &gt;, como muestra el ejemplo siguiente,

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";
if (isset($_REQUEST["nombre"])) {
    $nombre = trim(strip_tags(htmlspecialchars($_REQUEST["nombre"])));
} else {
    $nombre = "";
}

if ($nombre == "") {
    print "  <p>No ha escrito ningún nombre</p>\n";
} else {
    print "  <p>Su nombre es $nombre</p>\n";
}
?>
<pre>Array
(
    [nombre] =>
)
</pre>

  <p>No ha escrito ningún nombre</p>
  <pre>
Array
(
    [nombre] =>       <strong>intel&microsoft</strong>
)
</pre>

  <p>Su nombre es &lt;strong&gt;intel&amp;microsoft&lt;/strong&gt;</p>
Array
(
    [nombre] =>       intelµsoft
)

Su nombre es <strong>intel&microsoft</strong>

Enlace a ejemplo

Ya que la función strip_tags() no hace nada, el ejemplo anterior se podría simplificar eliminando la función strip_tags():

<?php
print "  <pre>\n";
print_r($_REQUEST);
print "</pre>\n";
if (isset($_REQUEST["nombre"])) {
    $nombre = trim(htmlspecialchars($_REQUEST["nombre"]));
} else {
    $nombre = "";
}

if ($nombre == "") {
    print "  <p>No ha escrito ningún nombre</p>\n";
} else {
    print "  <p>Su nombre es $nombre</p>\n";
}
?>
<pre>Array
(
    [nombre] =>
)
</pre>

  <p>No ha escrito ningún nombre</p>
  <pre>
Array
(
    [nombre] =>       <strong>intel&microsoft</strong>
)
</pre>

  <p>Su nombre es &lt;strong&gt;intel&amp;microsoft&lt;/strong&gt;</p>
Array
(
    [nombre] =>       intelµsoft
)

Su nombre es <strong>intel&microsoft</strong>

Enlace a ejemplo

Función de recogida de datos

La forma más cómoda de tener en cuenta todos los aspectos comentados en los puntos anteriores es definir una función recoge():

La función recoge() se encuentra en la lección Función recoge().