Volver a los textos

El prompt no es una frontera de seguridad

Cuando un LLM escribe la consulta, autorizar a quien pregunta no basta. Importa lo que la respuesta puede tocar.

En los sistemas que generan SQL, filtros o código con un modelo de lenguaje, hay dos preguntas de autorización, y es fácil responder solo a la primera:

  • ¿Quién puede pedir la consulta?
  • ¿Qué puede tocar la consulta generada?

Autorizar la primera e ignorar la segunda es cerrar la puerta con llave y dejar la ventana abierta. El usuario tenía permiso para preguntar por los datos de su unidad; el SQL que escribió el modelo no lo sabe.

Carga solo lo que ya se autorizó

La protección más fuerte para que el modelo no acceda a lo que no debe es que el dato no esté ahí. En lugar de abrir un engine con todos los contextos y confiar en que el modelo elija el correcto, la ejecución recibe solo lo que ya pasó por la autorización:

ctx = authorize_data_context(user, context_id)  # predicado del servidor
engine = load_engine_only(ctx.storage_prefix)

Si el proceso de esa ejecución no ve los otros datasets, ningún prompt, por creativo que sea, hace que el modelo llegue a ellos.

El system prompt es una sugerencia

“Usa solo estas tablas” en el system prompt no es un control. El modelo es un componente probabilístico y, tarde o temprano, va a hacer otra cosa, ya sea por error o porque alguien se lo pidió con maña. La salida tiene que pasar por una validación determinista antes de ejecutarse:

pregunta
  -> contexto autorizado
  -> el modelo genera un candidato
  -> parser / AST / validación
  -> rechazo si es inválido
  -> auditoría
  -> ejecución en un engine acotado
  -> respuesta

El guard mínimo

Para SQL generado, lo mínimo que considero aceptable:

  • un único statement;
  • solo lectura, con DDL, DML y comandos administrativos bloqueados;
  • ninguna función de archivo ni de red;
  • toda relación referenciada dentro de la allowlist del contexto;
  • límite de filas, timeout y presupuesto de memoria;
  • conexión read-only, aunque el guard falle.

El último punto es el que da tranquilidad: el guard es una capa, la conexión read-only es otra y el engine acotado es una tercera. Ninguna tiene que ser perfecta por sí sola.

La regex es un andamio

Se puede empezar con regex, siempre que esté cubierta por tests adversariales. Pero la regex es un piso temporal, no un destino. Los comentarios, los strings, los identificadores entre comillas y la sintaxis específica de cada dialecto vuelven frágil cualquier regex:

SELECT * FROM "public"."Pacientes" /* ; DROP */
SELECT * FROM report WHERE note = 'FROM secrets'

Un parser compatible con el dialecto que va a ejecutar la query convierte la pregunta “¿esto parece peligroso?” en “¿qué relaciones referencia este árbol?”, y esa segunda pregunta tiene una respuesta exacta.

La inyección viene del dato

La pregunta del usuario no es la única entrada no confiable. Nombre de columna, valor de celda, metadato de hoja de cálculo, texto de un PDF ingerido: todo eso llega al contexto del modelo y puede contener algo que parece una instrucción. Delimitar esos elementos como datos ayuda, pero la defensa decisiva sigue siendo validar la salida. Si la respuesta solo puede ser un SELECT sobre relaciones autorizadas, una instrucción escondida en una hoja de cálculo no tiene adónde ir.

Menos herramientas, menos superficie

En el camino de consulta sensible, el agente recibe solo lo necesario. Si la tarea es lectura analítica, nada de escritura en el filesystem, red arbitraria, shell, Python libre, mutaciones de base de datos ni credenciales de otro servicio. El mínimo privilegio vale para los agentes igual que para las personas, y con más razón: el agente lee todo lo que le pasa por delante.

Esto dialoga con Enmascara antes de llamar al LLM: una capa se ocupa de lo que el modelo ve, la otra de lo que puede hacer.