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.
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.
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):
<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.
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.
<?php
print " <p>Su nombre es $_REQUEST[nombre]</p>\n";
?>
Su nombre es Pepito Conejo
<?php
print " <p>Su nombre es $_REQUEST["nombre"]</p>\n";
?>
<?php
print " <p>Su nombre es $_REQUEST['nombre']</p>\n";
?>
<?php
print " <p>Su nombre es " . $_REQUEST["nombre"] . "</p>\n";
?>
Su nombre es Pepito Conejo
<?php
print " <p>Su nombre es " . $_REQUEST['nombre'] . "</p>\n";
?>
Su nombre es Pepito Conejo
<?php
print " <p>Su nombre es " . $_REQUEST[nombre] . "</p>\n";
?>
<?php
print " <p>Su nombre es {$_REQUEST["nombre"]}</p>\n";
?>
Su nombre es Pepito Conejo
<?php
print " <p>Su nombre es {$_REQUEST['nombre']}</p>\n";
?>
Su nombre es Pepito Conejo
<?php
print " <p>Su nombre es {$_REQUEST[nombre]}</p>\n";
?>
En ningún caso se pueden utilizar los caracteres especiales \" o \', ni dentro ni fuera de las cadenas.
<?php
print " <p>Su nombre es $_REQUEST[\"nombre\"]</p>\n";
?>
<?php
print " <p>Su nombre es " . $_REQUEST[\'nombre\'] . "</p>\n";
?>
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.
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:
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:
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:
<?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
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):
<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:
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 ... :
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.
<?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
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.
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>
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";
?>
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).
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.
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:
<?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
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.
Si el usuario escribe en una entrada el carácter & (ampersand, entidad de carácter &) 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 &.
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 µ µ).
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µsoft</p>
La solución es sustituir el carácter & por su entidad de carácter correspondiente (&). Eso se puede hacer con la función str_replace()
<?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('&', '&', $_REQUEST["nombre"]) . "</p>\n";
}
?>
Su nombre es intelµsoft
<pre>
Array
(
[nombre] => intelµsoft
)
</pre>
<p>Su nombre es intel&microsoft</p>
Si el usuario escribe en una entrada el carácter " (comillas, entidad de carácter "), 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.
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 ("). Eso se puede hacer con la función str_replace()
<?php
if ($_REQUEST["nombre"] == "") {
print " <p>No ha escrito ningún nombre</p>\n";
} else {
print " <p>Corrija: <input type=\"text\" value=\"" . str_replace('"', '"', $_REQUEST["nombre"]) . "\"></p>\n";
}
?>
Corrija:
<pre>
Array
(
[nombre] => Me llamo "Pepe"
)
</pre>
<p>Corrija: <input type="text" value="Me llamo "Pepe""></p>
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.
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:
<?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µsoft</strong>
)
</pre>
<p>Su nombre es intel&microsoft</p>
Array
(
[nombre] => intelµsoft
)
Su nombre es intelµsoft
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 >, 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µsoft</strong>
)
</pre>
<p>Su nombre es <strong>intel&microsoft</strong></p>
Array
(
[nombre] => intelµsoft
)
Su nombre es <strong>intelµsoft</strong>
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µsoft</strong>
)
</pre>
<p>Su nombre es <strong>intel&microsoft</strong></p>
Array
(
[nombre] => intelµsoft
)
Su nombre es <strong>intelµsoft</strong>
La forma más cómoda de tener en cuenta todos los aspectos comentados en los puntos anteriores es definir una función recoge():
trim(htmlspecialchars($_REQUEST[$var]))
De esta manera, si se reciben etiquetas (<>), las etiquetas no se borrarán, sino que se conservarán como texto.
Si se quisieran borrar las etiquetas, habría que aplicar además la función strip_tags():
trim(htmlspecialchars(strip_tags($_REQUEST[$var])))
La función recoge() se encuentra en la lección Función recoge().