Un buscador que funciona. Alguien escribe un signo de porcentaje para probar si filtra bien y la lista no se reduce: sale todo. No hay error, no hay registro raro, y la primera página de resultados se ve perfectamente normal.
El comodín no lo para el parámetro ligado
Un campo de búsqueda sobre texto casi siempre termina en un LIKE o un ILIKE, y termina en uno con el patrón %término%. La documentación de PostgreSQL explica por qué: la comparación cubre la cadena entera, así que para encontrar algo en medio de un texto el patrón tiene que empezar y acabar en porcentaje.
El valor viaja como parámetro ligado, que es la defensa contra la inyección que OWASP pone en primer lugar. Ahí está la confusión que hace caer esto: ligar el parámetro protege la forma de la consulta, no el significado del patrón. El valor llega al motor como dato, y una vez dentro LIKE lo lee como lo que es, un patrón. En él, el guion bajo equivale a un carácter cualquiera y el porcentaje a cualquier secuencia de cero o más caracteres.
Así que quien escribe un porcentaje no está buscando ese carácter: está pidiendo un comodín. Y quien busca una referencia que lleva un guion bajo recibe también las vecinas que no coinciden en esa posición.
Lo caro de este fallo es que no hace ruido. No lanza una excepción ni ensucia los registros: devuelve una lista. Con resultados paginados, la primera página de “todo” se parece bastante a la primera página de “lo que pediste”, y quien está al otro lado actúa sobre el registro equivocado creyendo que buscó bien.
Borrar los caracteres parece la salida y es la peor
La corrección de reflejo es quitar el porcentaje y el guion bajo de lo que escribió la persona. Deja de devolver toda la tabla, y por eso parece que funciona.
Lo que hace en realidad es cambiar la pregunta sin decírselo a nadie. Una búsqueda de A_01 se convierte en una de A01 y trae otra cosa, o nada, y quien la hizo concluye que ese registro no existe. Rechazar la entrada con un mensaje es peor todavía: los dos caracteres son texto legítimo y aparecen en referencias, en nombres de archivo y en direcciones de correo. Un buscador que prohíbe caracteres es un buscador roto con una disculpa.
Escapar el patrón, y declarar con qué carácter
La salida está en el mismo manual: para buscar un guion bajo o un porcentaje literales hay que precederlos del carácter de escape, que por defecto es la barra invertida, y se puede elegir otro con la cláusula ESCAPE. De ahí salen tres detalles que deciden si queda bien o a medias.
- Se escapan tres caracteres, no dos. El carácter de escape va primero, y después el porcentaje y el guion bajo. Si solo se escapan los comodines, una barra invertida escrita por la persona se come el escape siguiente y el comodín vuelve a entrar.
- Declarar
ESCAPEsiempre, aunque sea el valor por defecto. El manual avisa de que esta parte de PostgreSQL es ligeramente no estándar: según el estándar SQL, omitirESCAPEsignifica que no hay carácter de escape, no que sea la barra invertida. El mismo patrón contra otro motor, o pasando por una capa que no lo declara, vuelve a ser un comodín. Declararlo no cuesta nada y quita la pregunta de encima. Conviene saber además que una cláusula de escape vacía desactiva el mecanismo entero y deja los comodines imposibles de apagar. - No esperes que lo haga el ayudante de tu capa de datos. La documentación de SQLAlchemy, por ejemplo, muestra que su operador de contenido se traduce a un
LIKEque concatena el valor entre dos porcentajes: arma el patrón, no escapa el valor. Es el comportamiento razonable, porque la biblioteca no sabe si lo que recibe es un patrón o un texto. Decidirlo es tuyo.
Qué se lleva cualquiera que tenga un campo de búsqueda
La prueba cuesta un minuto y no necesita leer código: escribe un porcentaje, un guion bajo y una barra invertida en tu propio buscador. Hay tres resultados que delatan el problema: que salga todo, que salga una lista equivocada pero creíble, o que salte un error. En los tres casos el patrón se está armando con lo que escribió la persona.
Y la lección que sirve fuera de este caso: “parámetro ligado” y “entrada escapada” resuelven dos problemas distintos, y el primero se da por hecho con tanta razón que tapa al segundo. Un campo de texto que acaba en un lenguaje de patrones es una traducción, y toda traducción tiene caracteres que hay que marcar. Lo mismo vale para las expresiones regulares y para los comodines de un sistema de archivos.
Esta es la clase de detalle que aparece operando y no diseñando, como el candado que toma una clave foránea cuando dos escrituras caen sobre la misma fila. En IntegraLaIA operamos agentes con clientes reales, y los casos están en casos; cómo tratamos los datos y los accesos, en seguridad.