En resumen: las entradas en PHP se validan con filter_var() o filter_input_array() y reglas claras para cada campo: tipo, rango, longitud y valores permitidos. Lo que no encaja se rechaza, no se remodela en silencio. La validación es importante para tener datos limpios y menos superficie de ataque, pero no sustituye ni a las consultas preparadas contra la inyección SQL ni a la codificación adecuada al contexto contra XSS.
Por qué «filtrarlo todo» no es un concepto
En muchos proyectos PHP antiguos se encuentra al principio de cada archivo un bucle que pasa todos los valores de $_GET y $_POST por una función de saneamiento. Da sensación de seguridad. En la práctica, unas veces elimina demasiado, por ejemplo los apóstrofos de los apellidos, y otras demasiado poco, porque la misma función debe valer a la vez para SQL, HTML y nombres de archivo.
Una validación sensata funciona de otro modo: pregunta, para cada campo, qué está permitido. Una cantidad de pedido es un número entero entre 1 y un límite superior. Un campo de ordenación es uno de tres valores fijos. Una fecha tiene un formato determinado y es posterior a otra fecha. Todo lo demás se rechaza.
OWASP distingue aquí dos niveles: la comprobación sintáctica, si un valor tiene la forma correcta, y la comprobación semántica, si tiene sentido en el contexto. Ambas pertenecen al servidor; las comprobaciones en el navegador solo sirven para la usabilidad.
Qué filtros trae PHP
La extensión de filtros viene incluida en PHP por defecto. Los filtros de validación más importantes según el manual de PHP:
| Filtro | Comprueba | Opciones importantes y observaciones |
|---|---|---|
FILTER_VALIDATE_INT |
número entero | min_range, max_range |
FILTER_VALIDATE_FLOAT |
número decimal | decimal para el separador |
FILTER_VALIDATE_BOOL |
valor booleano | con FILTER_NULL_ON_FAILURE se distingue de «falso» |
FILTER_VALIDATE_EMAIL |
forma de una dirección de correo | no demuestra que el buzón exista |
FILTER_VALIDATE_URL |
forma de una URL | comprobar el esquema por cuenta propia; no admite dominios internacionalizados |
FILTER_VALIDATE_IP |
dirección IP | flags para rangos privados y reservados |
FILTER_VALIDATE_REGEXP |
patrón propio | anclar el patrón con ^ y $ |
El filtro de saneamiento FILTER_SANITIZE_STRING está obsoleto desde PHP 8.1. Quien aún lo use debería migrar la validación a filtros adecuados y emplear htmlspecialchars() en la salida.
filter_var en PHP: comprobar bien un formulario
Con filter_input_array() puede comprobarse un formulario entero de una vez frente a una definición. Los campos desconocidos no se aceptan:
<?php
declare(strict_types=1);
$regeln = [
'menge' => ['filter' => FILTER_VALIDATE_INT,
'options' => ['min_range' => 1, 'max_range' => 50]],
'email' => FILTER_VALIDATE_EMAIL,
'agb' => ['filter' => FILTER_VALIDATE_BOOL, 'flags' => FILTER_NULL_ON_FAILURE],
'versand' => ['filter' => FILTER_VALIDATE_REGEXP,
'options' => ['regexp' => '/^(standard|express)$/']],
];
$daten = filter_input_array(INPUT_POST, $regeln) ?? [];
$fehler = [];
foreach ($regeln as $feld => $_) {
if (!array_key_exists($feld, $daten) || $daten[$feld] === false || $daten[$feld] === null) {
$fehler[] = $feld;
}
}
if ($fehler !== []) {
http_response_code(422);
// Mostrar el mensaje de error de cada campo, no procesar las entradas
}
Fíjese en las comparaciones estrictas con ===: un valor válido 0 o una cadena vacía no debe confundirse con un fallo. Por qué las comparaciones laxas son aquí peligrosas lo explica Type juggling: las comparaciones laxas como riesgo de seguridad. Cómo pone PHP a disposición los datos de la petición lo describe Superglobales: $_GET, $_POST, $_FILES y compañía.
Trampas con el correo, las URL y los textos libres
Algunos filtros hacen menos de lo que sugiere su nombre:
- URL:
FILTER_VALIDATE_URLacepta también otros esquemas distintos de http y https. Compruebe después el esquema conparse_url()frente a una lista fija. Si el servidor va a recuperar la URL, rigen reglas adicionales; véase Evitar el Server-Side Request Forgery en PHP. - Direcciones de correo: el filtro comprueba la forma, no la titularidad. Quien necesite certeza envía un enlace de confirmación.
- Texto libre: nombres, mensajes y direcciones difícilmente pueden limitarse de forma razonable a ciertos caracteres. Compruebe la longitud y la codificación de caracteres, por ejemplo con
mb_check_encoding($wert, 'UTF-8'), y en la salida confíe en la codificación. - Expresiones regulares: sin
^y$, un patrón solo comprueba si aparece en alguna parte del texto, no si coincide el valor completo. - Arrays en lugar de cadenas: un atacante puede enviar un array en lugar de un texto. Sin comprobación de tipo, esto provoca advertencias o un comportamiento inesperado.
Para valores de selección fijos como ordenación, idioma o tipo de envío, una lista de permitidos con in_array($wert, $erlaubt, true) o una expresión match es más sencilla y más estricta que cualquier expresión regular.
Otro error frecuente afecta a los números con significado, como precios, cantidades o descuentos. Que un valor sea un número válido no dice nada sobre si está permitido en el contexto. Una cantidad negativa, un descuento superior al valor de la mercancía o un precio tomado del formulario en lugar de la base de datos son errores lógicos clásicos. Por ello, precios, permisos y valores de estado pertenecen siempre al servidor y nunca a campos ocultos del formulario.
JSON, webhooks e importaciones: validación más allá de los formularios
Hoy, muchas entradas ya no son formularios, sino peticiones JSON de apps, webhooks de servicios de pago o archivos de importación de proveedores. También aquí rige: primero leer con seguridad, después comprobar. OWASP aconseja limitar el tamaño y la profundidad de anidación antes siquiera de leer los datos. En PHP, json_decode() con una profundidad máxima y JSON_THROW_ON_ERROR se encarga de una parte:
<?php
$roh = file_get_contents('php://input', false, null, 0, 65536); // como máximo 64 KB
try {
$daten = json_decode($roh, true, 16, JSON_THROW_ON_ERROR);
} catch (JsonException) {
http_response_code(400);
exit;
}
if (!is_array($daten) || !is_string($daten['bestellnummer'] ?? null)) {
http_response_code(422);
exit;
}
En los webhooks, antes de la comprobación del contenido está la pregunta de si el mensaje procede realmente del servicio indicado. Si el servicio ofrece una firma, compruébela y compárela con hash_equals(). En los archivos de importación, compruebe cada línea como un formulario y aborte limpiamente ante errores, en lugar de aceptar registros a medias.
Los mensajes de error deberían nombrar el campo afectado, pero no devolver sin filtrar el valor rechazado. De lo contrario, el propio mensaje de error se convierte en un nuevo punto de salida que de nuevo debe codificarse.
Lo que la validación no puede lograr
La validación reduce la superficie de ataque, pero según OWASP no es la defensa principal contra la inyección. Un apellido con apóstrofo es válido y, aun así, debe llegar a una base de datos con seguridad. Eso solo se consigue con consultas preparadas, como muestra la guía Evitar la inyección SQL en PHP. Y un texto de comentario válido solo resulta inofensivo mediante la codificación adecuada al contexto en la salida.
El orden correcto es, pues: validar en la recepción, parametrizar en el procesamiento, codificar en la salida. Además, delante deben ir límites de tamaño: las peticiones demasiado grandes ni siquiera deberían ser procesadas por completo por el servidor web y PHP. Para ello sirven ajustes como post_max_size, que tras PHP2Shell conviene revisar de todos modos.
¿Cómo ayuda PHP SOS en concreto?
- Registramos todas las entradas de su aplicación: formularios, interfaces, subidas, importaciones y webhooks.
- Definimos para cada campo qué está permitido y sustituimos las funciones de saneamiento generales por comprobaciones claras.
- Comprobamos si los accesos a la base de datos y las salidas están protegidos con independencia de la validación.
- Documentamos las reglas de modo que su equipo pueda mantenerlas en las nuevas funciones.
Una revisión sistemática la describe el servicio Auditoría de seguridad y prueba de penetración.
Preguntas frecuentes
¿Qué diferencia hay entre validar y sanear?
Validar comprueba si un valor cumple lo esperado y, si no, lo rechaza. Sanear modifica el valor, por ejemplo eliminando caracteres. Para decisiones de seguridad, rechazar es casi siempre mejor, porque un valor modificado en silencio puede generar nuevos errores.
¿Se puede seguir usando FILTER_SANITIZE_STRING?
El filtro está marcado como obsoleto desde PHP 8.1. El manual de PHP recomienda en su lugar htmlspecialchars() en la salida. Debería migrar el código existente, porque las funciones obsoletas pueden desaparecer en versiones futuras.
¿Basta FILTER_VALIDATE_URL para que los enlaces sean seguros?
No. El filtro solo comprueba la forma de la dirección. Según el manual, una URL válida no tiene por qué empezar por http, así que debe comprobar usted mismo el esquema contra una lista de permitidos como https. El filtro rechaza los nombres de dominio internacionalizados.
¿Tengo que validar también los datos de mi propia base de datos?
En la salida se consideran no fiables y se codifican según corresponda. Una nueva validación tiene sentido cuando los datos procedían originalmente del exterior y ahora se usan en un contexto nuevo, por ejemplo como nombre de archivo o parte de un comando.
¿Ayuda la validación frente a PHP2Shell?
No. PHP2Shell afecta al procesamiento de peticiones en el intérprete de PHP, antes de que se ejecute siquiera el código de su aplicación. Frente a ello solo ayuda la actualización de PHP con el posterior reinicio de todos los procesos de PHP.