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.
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.
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.
<?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";
?>
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 no debe suponer que 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 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:
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:
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:
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
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:
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 ... :
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:
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.
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
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.
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";
?>
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>"; 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:
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
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:
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>
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):
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>
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).
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 &.
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 (&). Eso se puede hacer con la función str_replace()
Nombre:
<?php
$nombre = (isset($_REQUEST["nombre"]))
? trim(strip_tags($_REQUEST["nombre"]))
: "";
$nombre = str_replace('&', '&', $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>
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
$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 ("). Eso se puede hacer con la función str_replace()
Nombre:
<?php
$nombre = (isset($_REQUEST["nombre"]))
? trim(strip_tags($_REQUEST["nombre"]))
: "";
$nombre = str_replace('"', '"', $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:
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 < y > 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:
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().