Protectores de alcance para agentes de IA: Decodificación de IPs y dominios ocultos antes de ejecutar.
Protectores de alcance para agentes de IA: decodifican la codificación anidada en base64, hexadecimal y URL, normalizan los formatos de IP inusuales y filtran los nombres de archivos para que los objetivos fuera del alcance nunca se ejecuten.
Los diseños de agentes suelen tratar al agente como el componente seguro. Ejecuta código dentro de un entorno aislado propiedad del equipo, edita los archivos que se le han proporcionado y lee los registros que el equipo ha registrado. Hay una situación que rompe esa confianza: el agente trabaja a partir de texto escrito por otra persona, y parte de ese texto está diseñado para inducirlo a hacer algo más allá de sus instrucciones.
El escenario
Considere un modelo de lenguaje recursivo contratado para una prueba de penetración autorizada. En sus instrucciones se especifican los rangos de IP y dominios a los que puede acceder, y todo lo que esté fuera de esa lista está prohibido, independientemente de la ruta que el modelo utilice para llegar a ello. En algún momento, el modelo emite una orden de shell. Es necesario contar con un punto de control entre esa salida y el shell real, y este debe imponer ese límite por sí mismo en lugar de confiar en que el modelo lo recuerde.
Comparar la orden con una lista blanca de cadenas resulta insuficiente. Los atacantes, así como los modelos influenciados por el texto que han leído, rara vez escriben abiertamente una dirección prohibida. La ocultan, por lo que el punto de control debe ser tan bueno para revelarla como lo son ellos para ocultarla.
Escondido en la codificación
Supongamos que el único destino permitido es target.htb. Una verificación de alcance ingenua escanea la orden en busca de subcadenas que parezcan dominios y las compara con ese único valor. Funciona hasta que la orden tiene este aspecto:
curl http://localhost/exec?cmd=ping%20192.168.2.1
192.168.2.1 no aparece en texto plano, ya que está codificado con porcentajes dentro de la URL. Al decodificarlo una vez, se encuentra allí mismo. Si se envuelve en base64, se divide mediante escapes hexadecimales o se usan varias capas, una verificación que solo lee el texto literal pasa con todas las variantes, aunque cada una apunte al mismo host fuera del alcance.
Esto no es una peculiaridad de un único formato de codificación. Por lo general, un valor puede escribirse de muchas maneras equivalentes, y un validador que inspecciona la cadena superficial solo verifica una de ellas.
La solución es decodificar primero y juzgar después. Antes de que la verificación de alcance determine si una dirección está permitida, elimina recursivamente toda codificación que pueda encontrar y evalúa los valores en texto plano que resultan:
def _de_cloak_payloads(cmd: str, depth: int = 0, max_depth: int = 3) -> set[str]:
"""
Recursively searches for base64, hex, and URL encodings inside cmd.
Returns a set of all extracted/decoded plain strings.
"""
extracted = {cmd}
if depth >= max_depth:
return extracted
# 1. URL Decoding
decoded_url = urllib.parse.unquote(cmd)
if decoded_url != cmd:
extracted.update(_de_cloak_payloads(decoded_url, depth + 1, max_depth))
# 2. Hex escape and raw hex string decoding
for match in re.findall(r"(?:\\x[0-9a-fA-F]{2})+", cmd):
hex_bytes = bytes.fromhex(match.replace("\\x", ""))
extracted.update(_de_cloak_payloads(hex_bytes.decode("utf-8", errors="ignore"), depth + 1, max_depth))
# 3. Base64 decoding
for match in re.findall(r"\b[A-Za-z0-9+/]{12,}={0,2}\b", cmd):
padded = match + "=" * ((4 - len(match) % 4) % 4)
decoded = base64.b64decode(padded.encode("ascii")).decode("utf-8", errors="ignore")
extracted.update(_de_cloak_payloads(decoded, depth + 1, max_depth))
return extracted
La recursión introduce como entrada la salida de cada intento de decodificación en la misma función, con un límite en la profundidad. Imagina la codificación URL envuelta en base64, que a su vez está envuelta en escapes hexadecimales: se despliega paso a paso, y cada cadena generada en el proceso, no solo la más interna, se agrega al conjunto que luego se analiza.
Lo que resulta es un conjunto: la orden original más todos los valores puros que se pueden recuperar mediante decodificación, hasta una profundidad de tres capas. Cada uno se verifica en función del ámbito permitido. Si alguno contiene una dirección fuera del rango autorizado, toda la orden es rechazada, incluso si el texto literal del modelo nunca mencionó esa dirección.
Escondido en los formatos numéricos
La codificación no es el único disfraz. Una dirección IP es un número, y los números tienen varias formas de escritura válidas. 192.168.2.1 puede ser un único entero de 32 bits, cuatro partes en formato octal o hexadecimal, o una combinación con espacios adicionales que la mayoría de las herramientas aceptan. Una expresión regular ajustada para el formato decimal con puntos no reconoce ninguna de estas formas, pero una verdadera pila de red las resuelve como si se tratara de la misma máquina.
def _normalize_ip_token(token: str) -> str | None:
token = token.strip().lower()
# A bare integer or hex integer standing in for a full IP
if token.isdigit() or (token.startswith("0x") and all(c in "0123456789abcdef" for c in token[2:])):
val = int(token, 16) if token.startswith("0x") else int(token)
if 0 <= val <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:
if val > 1024 or val in (0,):
return str(ipaddress.ip_address(val))
# Dotted octal or dotted hex, one part at a time
if "." in token:
parts = token.split(".")
if len(parts) == 4:
normalized_parts = []
for p in parts:
val = int(p, 16) if p.startswith("0x") else int(p, 8) if p.startswith("0") and len(p) > 1 else int(p)
if 0 <= val <= 255:
normalized_parts.append(str(val))
if len(normalized_parts) == 4:
return ".".join(normalized_parts)
return None
Cada forma de escritura impar se reescribe en notación estándar con puntos antes de que se compare con los rangos permitidos. Eso significa que 0xC0A80201 y 192.168.2.1 se consideran el mismo host con diferentes “vestimentas”.
Dominios y nombres de archivo
La verificación de IP tiene como objetivo detectar disfrazamientos. La verificación de dominios presenta una dificultad inversa: la red debe ser lo suficientemente amplia como para identificar referencias de dominio reales sin marcar como sospechosas a todos los nombres de archivo que contengan un punto.
Una expresión regular diseñada para encontrar tokens con formato de dominio reconoce tanto config.yaml o report.pdf como evil.example.com, ya que cada uno tiene la misma estructura: letras, un punto y más letras. La decodificación adicional no ayuda en este caso. Lo que sí sirve es una segunda lista, compuesta por extensiones de archivo comunes, que se compara con lo que debería estar en el lugar del dominio de nivel superior.
tld = token.rsplit(".", 1)[-1]
if tld in _FILE_EXTENSIONS:
continue
if not any(token == d or token.endswith("." + d) for d in self.domains):
raise ScopeViolation(f"Domain {token!r} is outside engagement scope.")
La lista debe ajustarse, ya que ambos extremos son perjudiciales. Si es demasiado corta, las rutas de archivo comunes siguen activando alarmas, por lo que los usuarios aprenden a descartar por completo la herramienta. Si es demasiado larga, un dominio real que está fuera del alcance puede ocultarse detrás de un sufijo que casualmente esté excluido. La lista real abarca varias docenas de extensiones, desde py y sh hasta pem y elf, porque las operaciones involucran todo tipo de archivos y cada uno representa el riesgo de una alerta falsa.
La verificación completa hace pasar a cada candidato decodificado por ambos filtros antes de permitir que se ejecute una orden:
def check(self, cmd: str) -> None:
payloads = _de_cloak_payloads(cmd)
for payload in payloads:
if self.networks:
for m in _IPV4_RE.finditer(payload):
self._validate_ip(m.group(1))
for token in re.split(r"[\s\"'$,;()|&<>`\\/]", payload):
normalized = _normalize_ip_token(token)
if normalized:
self._validate_ip(normalized)
if self.domains:
for m in _DOMAIN_RE.finditer(payload):
token = m.group(0).lower()
tld = token.rsplit(".", 1)[-1]
if tld in _FILE_EXTENSIONS:
continue
if not any(token == d or token.endswith("." + d) for d in self.domains):
raise ScopeViolation(f"Domain {token!r} is outside engagement scope.")
Un valor problemático en cualquier variante decodificada detiene la orden. No es necesario demostrar cuál era la intención del modelo; basta con que una interpretación plausible del contenido de la orden llegue a la dirección prohibida.
Más allá de las pruebas de seguridad
Si se elimina el contexto de las pruebas de penetración, lo que queda es un patrón común a todos los agentes que procesan entrada no confiable. Piense en un flujo de aprobación que compara una cantidad con un límite de gasto, un filtro que verifica el texto enviado contra una lista de bloqueo, o una verificación de acceso que comprueba un recurso solicitado contra una lista de permisos. La implementación ingenua prueba cualquier cadena literal que llegue. La fiable primero normaliza todas las formas equivalentes del valor y luego prueba el resultado. Una protección basada en comandos de shell sirve como lección aplicable igualmente a los campos JSON, las rutas de archivos y los parámetros de URL cada vez que un agente decide si proceder o no.
El costo es la siguiente pregunta
Todo lo anterior se refiere a la corrección y al control: si el agente permanece dentro de los límites establecidos, si un archivo reparado sigue funcionando y si un valor decodificado puede evaluarse de manera segura. Nada de esto responde a una pregunta más cotidiana que surge en cuanto aparecen usuarios reales. Los sistemas recursivos realizan muchas llamadas al modelo, y muchas de esas llamadas consumen tiempo y dinero. De dónde proviene ese costo y qué es lo que realmente lo reduce son cuestiones separadas.
Recomendación práctica para quienes desarrollen un mecanismo similar: traten al decodificador y al normalizador como parte de los límites de seguridad, pruébenlos con entradas adversarias como codificaciones anidadas y direcciones de radicaje mixto, y mantengan bajo revisión la lista de extensiones permitidas. Una protección que era correcta el trimestre pasado puede volverse vulnerable tan pronto como nuevos tipos de archivos y herramientas se integren al flujo de trabajo.
Leer más
- Los sistemas multi-agente son sistemas backend con componentes no deterministas — Por qué los sistemas multi-agente en producción fomentan hábitos de tipo backend: contratos explícitos, prompts como configuración, contexto estricto, un rol por agente y código de fiabilidad sólido.
- Notas prácticas: Construyendo un agente UI autónomo con el nuevo Google — Guía paso a paso de las notas prácticas: Construyendo un agente UI autónomo con el nuevo Google: contratos, verificaciones y espacios para código listo para usar para los equipos que implementan este patrón.