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.

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 un ejemplo de formulario:

<form action="ejemplo.php">
  <p>Nombre: <input type="text" name="nombre"></p>
  <p><input type="submit" value="Enviar"></p>
</form>

Nombre:

Mientras se está programando, para comprobar que el fichero php está recibiendo la información enviada por el control, lo más fácil es utilizar la función print_r($matriz) para mostrar el contenido de la matriz $_REQUEST. Una vez se ha comprobado que la información llega correctamente, la línea se debe comentar o eliminar.

El siguiente ejemplo muestra lo que escribiría el programa PHP si recibiera la información del formulario anterior.

Nombre:

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

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

Su nombre es Pepito Conejo

Conviene colocar etiquetas <pre> alrededor del print_r($_REQUEST) para facilitar la visualización de los valores.

Referencia a $_REQUEST dentro y fuera de cadenas

Al hacer referencia a los elementos de la matriz $_REQUEST, 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 no debe suponer que 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 los controles no contengan ningún valor.

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.html
ejemplo.php
http:// ... /ejemplo.php?nombre=Pepito+Conejo

Nombre:

<?php
print "<pre>"; 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.html
ejemplo.php
http:// ... /ejemplo.php?nombre=

Nombre:

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

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

Su nombre es


Este problema se puede resolver incluyendo una estructura if ... else ... que considere la posibilidad de que no se haya escrito nada en el formulario:

ejemplo.html
ejemplo.php
http:// ... /ejemplo.php?nombre=

Nombre:

<?php
print "<pre>"; 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

Controles inexistentes: isset()

Un problema más grave es que el programa suponga que existe un control que en realidad no le ha llegado.

Eso puede ocurrir, por ejemplo, en el caso de las casillas de verificación y los botones radio, ya que los formularios envían el control solamente cuando se han marcado.

<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:

Si en el formulario anterior el usuario marca la casilla, el siguiente programa funciona perfectamente:

ejemplo.html
ejemplo.php
http:// ... /ejemplo.php?acepto=on

Deseo recibir información:

<?php
print "<pre>"; 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.html
ejemplo.php
http:// ... /ejemplo.php

Deseo recibir información:

<?php
print "<pre>"; 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 en realidad en cualquier formulario (incluso en formularios con cajas de texto que en principio envían siempre el control) ya que el usuario puede escribir directamente la dirección del programa PHP en el navegador y ejecutar el programa PHP sin pasar por el formulario. En ese caso, el programa no recibe ningún control y el programa genera un aviso por hacer referencia a un índice no definido:

ejemplo.html
ejemplo.php
http:// ... /ejemplo.php

Nombre:

<?php
print "<pre>"; 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
(
)

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

No ha escrito ningún nombre


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.html
ejemplo.php
http:// ... /ejemplo.php?acepto=on

Deseo recibir información:

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

Desea recibir información

ejemplo.html
ejemplo.php
http:// ... /ejemplo.php

Deseo recibir información:

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

No desea recibir información

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

Un usuario puede insertar código html en la entrada de un formulario, lo que puede acarrear comportamientos inesperados y riesgos de seguridad. El siguiente ejemplo solamente perjudicaría al aspecto de la página:

Nombre:

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

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

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.

Nombre:

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

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

Recogida de datos. Inyección de JavaScript

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.

Nombre:

<?php
print "<pre>"; 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>

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

Nombre:

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

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

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.html
ejemplo.php
http:// ... /ejemplo.php?nombre=

Nombre:

<?php
print "<pre>"; 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.html
ejemplo.php
http:// ... /ejemplo.php?nombre=

Nombre:

<?php
print "<pre>"; 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

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 los siguientes ejemplos, se puede ver cómo el programa no genera avisos y muestra el mensaje correspondiente al dato recibido:

ejemplo.html
ejemplo.php
http:// ... /ejemplo.php?nombre=   

Nombre:

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

if (isset($_REQUEST["nombre"])) {
    $nombre = trim(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>
ejemplo.html
ejemplo.php
http:// ... /ejemplo.php?nombre=<strong>Pepito+Conejo<%2Fstrong>

Nombre:

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

if (isset($_REQUEST["nombre"])) {
    $nombre = trim(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] => <strong>Pepito Conejo</strong>
)
</pre>
<p>Su nombre es Pepito Conejo</p>

La asignación de la variable se puede realizar en una sola instrucción, utilizando la notación abreviada: condición ? valor_1 : valor_2; (que se explica en la lección de estructuras de control):

ejemplo.html
ejemplo.php
http:// ... /ejemplo.php?nombre=<strong>Pepito+Conejo<%2Fstrong>

Nombre:

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

$nombre = (isset($_REQUEST["nombre"]))
    ? trim(strip_tags($_REQUEST["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] => <strong>Pepito Conejo</strong>
)
</pre>
<p>Su nombre es Pepito Conejo</p>

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).

La página es inválida 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
$nombre = (isset($_REQUEST["nombre"]))
    ? trim(strip_tags($_REQUEST["nombre"]))
    : "";

if ($nombre == "") {
    print "<p>No ha escrito ningún nombre</p>\n";
} else {
    print "<p>Su nombre es $nombre</p>\n";
}
?>
<p>Su nombre es Pepito & Company</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

Nombre:

<?php
$nombre = (isset($_REQUEST["nombre"]))
    ? trim(strip_tags($_REQUEST["nombre"]))
    : "";
$nombre = str_replace('&', '&amp;', $nombre);

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

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
$nombre = (isset($_REQUEST["nombre"]))
    ? trim(strip_tags($_REQUEST["nombre"]))
    : "";

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

Corrija:

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 es solamente "Me llamo":

<p>Corrige: <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

Nombre:

<?php
$nombre = (isset($_REQUEST["nombre"]))
    ? trim(strip_tags($_REQUEST["nombre"]))
    : "";
$nombre = str_replace('"', '&quot;', $nombre);

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

Corrija:

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.

Nombre:

<?php
$nombre = (isset($_REQUEST["nombre"]))
    ? htmlspecialchars(trim(strip_tags($_REQUEST["nombre"])))
    : "";

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

Corrija:

El ejemplo anterior tiene el inconveniente de que la primera función que se aplica es strip_tags() por lo que se eliminará todo lo que esté entre marcas (<>).

Nombre:

<?php
$nombre = (isset($_REQUEST["nombre"]))
    ? htmlspecialchars(trim(strip_tags($_REQUEST["nombre"])))
    : "";

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

Corrija:

Si no queremos quitar etiquetas, pero no queremos que se consideren como etiquetas sino como texto, habría que aplicar primero la función htmlspecialchars(), como en el ejemplo siguiente.

Nombre:

<?php
$nombre = (isset($_REQUEST["nombre"]))
    ? strip_tags(trim(htmlspecialchars($_REQUEST["nombre"])))
    : "";

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

Corrija:

En el ejemplo anterior la función strip_tags() no hace nada puesto que las marcas < y > se han cambiado antes por las entidades de carácter &lt; y &gt; así que se podría simplificar el programa:

Nombre:

<?php
$nombre = (isset($_REQUEST["nombre"]))
    ? trim(htmlspecialchars($_REQUEST["nombre"]))
    : "";

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

Corrija:

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().