# Alguien escribe % en tu buscador y recibe toda la tabla: el parámetro ligado no para los comodines

> Un parámetro ligado evita la inyección, pero no le quita el significado a % ni a _ dentro de un LIKE. Qué pasa cuando alguien los escribe en un buscador y cómo se escapan bien.

- Publicado: 11 de octubre de 2026
- Autor: Equipo de IntegraLaIA
- Categoría: Ingeniería
- Temas: Postgres, Producción
- Original: https://integralaia.com/blog/buscador-like-comodines-escape-postgres
- Cómo se hizo: Borrador asistido por IA (Claude); revisado, editado y verificado por Equipo de IntegraLaIA el 11 de octubre de 2026. Ilustración generada con IA.

---

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 `ESCAPE` siempre, 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, omitir `ESCAPE` significa 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 `LIKE` que 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](/blog/deadlock-postgres-clave-foranea) cuando dos escrituras caen sobre la misma fila. En IntegraLaIA operamos agentes con clientes reales, y los casos están en [casos](/casos); cómo tratamos los datos y los accesos, en [seguridad](/seguridad).
---

## Sobre qué se escribió

- PostgreSQL, documentación oficial, 9.7.1. LIKE (comodines, carácter de escape por defecto, cláusula ESCAPE y nota sobre el estándar SQL), consultado el 11 de octubre de 2026: https://www.postgresql.org/docs/current/functions-matching.html
- SQLAlchemy 2.0, Operator Reference, traducción de los operadores de contenido a LIKE, consultado el 11 de octubre de 2026: https://docs.sqlalchemy.org/en/20/core/operators.html
- OWASP, SQL Injection Prevention Cheat Sheet, Defense Option 1: Prepared Statements (with Parameterized Queries), consultado el 11 de octubre de 2026: https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html

IntegraLaIA (https://integralaia.com). Publicado bajo el enlace original; cítalo si lo reutilizas.
