← Todo el material
Curso Avanzado 24 de agosto de 2026

Seguridad de IA — Módulo 9: supply chain — modelos, pesos y dependencias

Noveno módulo del Curso de Seguridad de IA: la cadena de suministro de un sistema de IA como superficie de ataque. Modelos maliciosos, la trampa de los pesos en formato pickle, safetensors, model cards y licencias, dependencias de Python comprometidas, y cómo construir un SBOM para IA.

☰ Índice del contenido

En el módulo 8 blindamos los datos que entran al sistema: filtramos el corpus, aislamos por tenant, tratamos cada documento recuperado como texto no confiable. Todo ese trabajo asume que el modelo que consume esos datos y el código que lo ejecuta son de fiar. Este módulo cuestiona justamente eso. Antes de que un solo dato del usuario llegue a tu agente, ya tomaste una cadena de decisiones de confianza: descargaste unos pesos que entrenó un desconocido, instalaste veinte librerías que mantienen otros desconocidos, y corriste todo en tu infraestructura con tus credenciales. La pregunta del módulo no es qué entra a tu sistema en tiempo de ejecución, sino de dónde vino el sistema mismo.

Un aviso, porque el material es defensivo: todo lo que sigue sirve para auditar y endurecer tu propia cadena de suministro — los modelos que tú descargas, las dependencias que tú instalas, los artefactos que tú despliegas. No es una guía para envenenar hubs ajenos: es la lista de verificación de un ingeniero que no quiere ser la víctima.

La tesis del módulo

Descargar un modelo es ejecutar la confianza de un desconocido: los pesos pueden traer código, las dependencias pueden estar comprometidas y el “open weight” no significa “auditado”. La cadena de suministro es tan segura como su eslabón menos verificado, y casi nadie verifica el primero.

La cadena de suministro de un sistema de IA

En software tradicional la cadena de suministro (supply chain, todo lo que entra a tu producto sin que lo escribieras tú) ya era un problema serio: paquetes de npm, imágenes de Docker, dependencias transitivas. Un sistema de IA agrega dos eslabones nuevos que la mayoría de los pipelines de seguridad clásicos no saben mirar: los pesos del modelo y el dataset con que se entrenó. Ninguno de los dos es código en el sentido tradicional, pero ambos pueden cargar comportamiento malicioso, y ninguno aparece en un análisis de vulnerabilidades convencional.

      CADENA DE SUMINISTRO DE UN SISTEMA DE IA

  ┌─────────┐     ┌────────────┐     ┌─────────┐
  │ dataset │ ──► │  entrena/  │ ──► │  pesos  │
  │ (crudo) │     │ fine-tune  │     │(artefac)│
  └────┬────┘     └─────┬──────┘     └────┬────┘
       │                │                 │
   poisoning        backdoor          pickle =
   del corpus       en el modelo      RCE al cargar
                                           │
                                           ▼
  ┌─────────┐     ┌────────────┐     ┌─────────┐
  │ tu app  │ ◄── │ dependenc. │ ◄── │   hub   │
  │(deploy) │     │   Python   │     │(público)│
  └─────────┘     └─────┬──────┘     └────┬────┘
                        │                 │
                 typosquatting      typosquatting
                 de paquetes        de repos + card
                                    falsa / licencia

Cada flecha de ese diagrama es un traspaso de confianza, y cada etiqueta de abajo es un ataque que ocurre en ese traspaso. El error mental que hay que corregir es tratar la caja “pesos” como si fuera un archivo de datos inerte. No lo es: en el formato equivocado, cargar esa caja es ejecutar código.

Riesgo 1 — Deserialización insegura: el pickle que ejecuta código

Este es el eslabón que más gente ignora y el más grave. PyTorch guardó históricamente sus pesos con pickle (el formato de serialización de objetos de Python). El problema es de diseño: pickle no guarda solo datos, guarda instrucciones para reconstruir objetos, y esas instrucciones pueden incluir llamadas arbitrarias. Un archivo .bin o .pt malicioso puede definir un método __reduce__ que ejecuta os.system(...) en el momento exacto en que lo cargas.

Traducido: torch.load() sobre un archivo no confiable es RCE (Remote Code Execution, ejecución remota de código: el atacante corre lo que quiera en tu máquina). No hace falta que ejecutes el modelo ni que le pases un input. Basta con abrirlo. Tus credenciales de nube, tus variables de entorno, tus llaves — todo lo que el proceso pueda tocar, lo puede tocar el payload.

Ojo: desde PyTorch 2.6 el valor por defecto de weights_only en torch.load pasó de False a True, lo que activa un unpickler restringido que se niega a importar módulos arbitrarios. Es una mejora real, pero no te salva si corres código viejo que fuerza weights_only=False, ni en versiones anteriores, ni si el atacante encuentra un gadget dentro de los tipos permitidos. El default seguro es un piso, no un techo.

Safetensors: el formato que no ejecuta nada

La respuesta estructural es cambiar de formato. Safetensors (el formato de serialización de Hugging Face) guarda solo tensores numéricos: un encabezado de 8 bytes con el largo de un bloque JSON de metadatos (nombres, formas, tipos, offsets), y después los bytes crudos de los pesos. No hay opcodes, no hay __reduce__, no hay forma de meter una llamada a función. Cargar un safetensors no puede ejecutar código, por construcción — la garantía no depende de un escáner que adivine, depende de que el formato no tiene dónde esconder una instrucción.

Por eso safetensors es hoy el formato por defecto en el Hub y en motores como Transformers y vLLM. La regla operativa: prefiere safetensors siempre; trata cualquier .bin/.pt/.ckpt de origen no verificado como código no confiable.

# CORRECTO: safetensors no puede ejecutar código al cargar.
from safetensors.torch import load_file

# Devuelve un dict {nombre_tensor: tensor}. No hay deserialización
# de objetos arbitrarios: solo se leen números.
state_dict = load_file("modelo.safetensors")
model.load_state_dict(state_dict)


# SI ESTÁS OBLIGADO a cargar un pickle (.bin/.pt), al menos:
import torch

# weights_only=True usa un unpickler restringido que se niega a
# importar módulos arbitrarios. Es el default desde PyTorch 2.6,
# pero decláralo explícito para no depender de la versión instalada.
state_dict = torch.load("modelo.bin", weights_only=True)

# Nunca hagas esto con un artefacto que no verificaste tú:
# torch.load("modelo.bin")                 # weights_only=False implícito
# torch.load("modelo.bin", weights_only=False)  # RCE si es malicioso

Si de verdad no puedes evitar el pickle no confiable, existe picklescan (escáner que Hugging Face corre sobre cada archivo subido): revisa los opcodes GLOBAL y marca como sospechosos los que importan módulos peligrosos contra una lista negra. Sirve como red de seguridad, pero no confíes en él como única defensa: es una lista negra, y se ha demostrado que se evade — por ejemplo comprimiendo el pickle con 7z en vez del ZIP de PyTorch, o usando gadgets fuera de la lista. Un escáner que pasa no prueba que el archivo sea seguro; solo prueba que no cayó en un patrón conocido.

Riesgo 2 — Modelos maliciosos y backdoors en hubs públicos

Un hub público como Hugging Face es, para la seguridad, lo que PyPI o npm son para las dependencias: un mercado abierto donde cualquiera publica. Dos vectores concretos:

  • Typosquatting de repos. El atacante publica meta-llama/Llama-3-8b-lnstruct (con L minúscula donde va una I) o mistralia/Mistral-7B, esperando el from_pretrained con un dedo torpe. El nombre casi correcto es el ataque completo.
  • Modelos troyanizados. Un modelo con backdoor (puerta trasera: comportamiento oculto que se activa con un disparador específico) parece normal en toda evaluación, hasta que recibe la frase gatillo y entonces filtra datos, aprueba lo que debía rechazar o ejecuta la acción del atacante. Si además viene en pickle, ni siquiera necesita el gatillo: el daño ocurre al cargarlo.

Se han documentado modelos con backdoor silencioso subidos a Hugging Face precisamente aprovechando la carga de pickle. El punto no es que el Hub sea inseguro — es que el origen de un artefacto es un dato de seguridad tan importante como su contenido, y “lo bajé del Hub” no es procedencia verificada.

Riesgo 3 — Poisoning aguas arriba (OWASP LLM04)

Los dos riesgos anteriores viven en el artefacto que descargas. Este vive antes, en cómo se fabricó. El data/model poisoning (envenenamiento: manipular los datos de entrenamiento o fine-tuning para instalar comportamiento indeseado) es la entrada LLM04 del OWASP Top 10 para LLM que ya vimos en el módulo 2. Si el corpus de pre-entrenamiento incluía páginas que un atacante plantó, o si el dataset de fine-tuning que descargaste trae ejemplos diseñados para crear un backdoor, el modelo llega envenenado de fábrica y ningún escaneo del archivo lo revela: el peso es “legítimo”, el comportamiento no.

Tú rara vez controlas el pre-entrenamiento de un modelo base, pero sí controlas el fine-tuning y los datasets que eliges. La defensa concreta: trata cada dataset descargado como corpus no confiable (verifica su hash, revisa su procedencia, no hagas fine-tuning sobre datos que no puedas auditar al menos por muestreo).

Riesgo 4 — Dependencias de Python comprometidas

El stack de ML es enorme: transformers, torch, accelerate, datasets, tokenizers, y detrás de ellas un árbol transitivo de cientos de paquetes. Cada uno es código que corre con tus permisos. Los vectores clásicos de la cadena de suministro de software aplican intactos:

  • Typosquatting de paquetes. pip install trasformers (falta una n) o python-transformers en vez del nombre real. El paquete falso instala y ejecuta lo que quiera.
  • Cuentas de mantenedor comprometidas que publican una versión con payload, o dependencias transitivas que nadie fijó y que de un día para otro traen código nuevo.

Esto conecta directo con una práctica ya establecida en el sitio: fijar versiones. El version pinning (fijar la versión exacta de cada dependencia, no un rango abierto) no es solo para reproducibilidad de builds — es control de cadena de suministro. Un transformers>=4.0 acepta silenciosamente cualquier versión futura, incluida la que un atacante publique tras comprometer una cuenta; un transformers==4.44.2 con hash verificado, no.

Riesgo 5 — Model cards, licencias y procedencia

El último eslabón es de gobernanza, no de ejecución, pero rompe proyectos igual. “Open weight” no es “auditado ni libre de restricciones”. Que puedas descargar los pesos (qué significa exactamente eso está en fundamentos) no dice nada sobre:

  • Auditoría. Nadie garantiza que esos pesos no tengan un backdoor. “Abierto” significa disponible, no revisado.
  • Licencia. Muchos modelos “abiertos” traen restricciones de uso comercial, cláusulas de uso aceptable o límites de usuarios activos. Desplegar sin leer la licencia es una deuda legal que estalla en producción.
  • Procedencia. La model card (ficha del modelo: el documento que describe datos, entrenamiento, sesgos y limitaciones) es autodeclarada. Es información valiosa, pero no es prueba: dice lo que el autor decidió decir.

La procedencia real se verifica con firma y atestación, no leyendo la card. Herramientas como Sigstore (firma de artefactos con transparencia pública) y estándares como SLSA (marco de niveles de integridad de la cadena de suministro) permiten atar un artefacto a quién lo produjo y cómo. El objetivo: poder responder “¿de dónde salió exactamente este peso y quién responde por él?” con algo más que “confié en el nombre del repo”.

Defensa: verificar el hash de lo que descargas

Antes de cualquier firma sofisticada está el control más barato y más olvidado: comprobar que el archivo que bajaste es byte por byte el que esperabas. Un hash (resumen criptográfico: una huella de tamaño fijo que cambia por completo si cambia un solo byte del archivo) que no coincide significa que el artefacto fue alterado, corrompido o sustituido en el camino.

"""Verificación de integridad de un artefacto descargado.

Uso defensivo: confirma que un peso/dataset que bajaste coincide con
el hash que publicó una fuente en la que decidiste confiar. Un hash
que no cuadra = no lo cargues, punto.
"""
import hashlib
from pathlib import Path


def sha256_de_archivo(ruta: str, bloque: int = 1 << 20) -> str:
    """Lee el archivo por bloques (los pesos pesan GB, no caben en RAM)
    y devuelve su SHA-256 en hexadecimal."""
    h = hashlib.sha256()
    with open(ruta, "rb") as f:
        for trozo in iter(lambda: f.read(bloque), b""):
            h.update(trozo)
    return h.hexdigest()


def verificar_o_abortar(ruta: str, hash_esperado: str) -> None:
    """El hash esperado viene de una fuente confiable que NO es el
    mismo lugar de descarga (si el atacante controla ambos, verificar
    no sirve). Idealmente firmado."""
    real = sha256_de_archivo(ruta)
    if real != hash_esperado.lower():
        raise SystemExit(
            f"HASH NO COINCIDE para {Path(ruta).name}\n"
            f"  esperado: {hash_esperado}\n"
            f"  obtenido: {real}\n"
            f"  -> artefacto alterado o corrupto: NO cargar."
        )
    print(f"OK  {Path(ruta).name}  sha256={real[:16]}...")


# Uso:
verificar_o_abortar(
    "modelo.safetensors",
    "9f2c...hash_publicado_por_la_fuente...e41a",
)

El detalle que hace o rompe este control está en el comentario: el hash esperado tiene que venir de un canal distinto al de la descarga. Si el atacante controla el mismo servidor del que bajas el peso y del que lees el hash, puede alterar ambos y la verificación te miente. Por eso la firma criptográfica (Sigstore) es mejor que un hash publicado al lado del archivo: ata el artefacto a una identidad, no a un servidor.

Defensa: un SBOM que incluya modelos y datasets

Un SBOM (Software Bill of Materials, lista de materiales de software: el inventario completo de todo lo que compone tu sistema, con versiones y hashes) es práctica estándar en seguridad de software. Para IA hay que extenderlo: el inventario no se cierra en las librerías de Python, tiene que incluir cada modelo y cada dataset, con su versión, su hash, su fuente y su licencia. Sin ese inventario no puedes responder la pregunta que llega el día del incidente: “salió una CVE en la versión X de tal librería / se reportó backdoor en tal modelo — ¿lo estamos usando y dónde?”

# sbom-ia.yaml  —  inventario extendido para un sistema de IA.
# Cada entrada verificable por hash y atada a una fuente y licencia.
componentes:
  modelos:
    - nombre: mi-clasificador-intencion
      version: "2026.08.01"
      formato: safetensors        # NO pickle: se rechaza en CI
      sha256: "9f2c...e41a"
      fuente: "hf://miorg/clasificador@a1b2c3d"
      licencia: "Apache-2.0"
      firmado: true               # atestación Sigstore verificada
  datasets:
    - nombre: intenciones-soporte
      version: "v7"
      sha256: "3d81...77ab"
      fuente: "interno://data-lake/intenciones"
      revisado_por_muestreo: true
  dependencias:
    - nombre: transformers
      version: "4.44.2"           # pineada, no rango abierto
      sha256: "a0b1...9c2d"
    - nombre: torch
      version: "2.6.0"
      sha256: "5e6f...1a2b"

Ese archivo no es documentación muerta: es un artefacto que se valida en CI. Una comprobación automática puede rechazar el build si un modelo llega en formato pickle, si a un componente le falta el hash, o si aparece una dependencia que no está en el inventario. Es la misma filosofía de las tablas de gobernanza de la clase 3 del harness: la seguridad que no está en el pipeline no existe.

Aislar la carga de lo no confiable

Cuando por la razón que sea tienes que cargar un artefacto que no pudiste verificar del todo — un modelo de un tercero, un pickle heredado —, la defensa de último recurso es no cargarlo donde puede hacer daño. Aislar la carga en un entorno con permisos mínimos, sin credenciales, sin red de salida, y con el sistema de archivos acotado, convierte un RCE de “acceso a toda tu nube” en “acceso a una caja vacía y descartable”. Ese sandboxing es justamente el tema del módulo que viene, así que aquí basta la regla: lo no confiable se carga preso, no suelto.

Eslabón × riesgo × control

EslabónRiesgo principalControl de primera líneaControl estructural
DatasetPoisoning aguas arriba (LLM04)Verificar hash + revisar por muestreoSolo datasets con procedencia auditable
EntrenamientoBackdoor instalado en el fine-tuneDatos de entrenamiento controladosReproducibilidad y registro del proceso
Pesos (formato)RCE por pickle al cargarweights_only=True / picklescanExigir safetensors; rechazar pickle en CI
Hub públicoTyposquatting / modelo troyanizadoVerificar nombre exacto y hashFirma/atestación (Sigstore, SLSA)
DependenciasPaquete comprometido / typosquatPinnear versión + hashEscaneo de CVE en CI + árbol auditado
Model card / licencia”Abierto” ≠ auditado ni libreLeer licencia antes de desplegarSBOM con licencia por componente

La columna de la derecha vuelve a contar la misma historia del curso: la defensa no vive en un chequeo manual que alguien recuerda hacer, vive en el pipeline que lo hace por ti. Un ingeniero que “sabe” que hay que preferir safetensors se olvida un martes cualquiera; un CI que rechaza el build ante un .bin no se olvida nunca.

Errores comunes

  • torch.load sobre un peso de origen no verificado. Es RCE, no una lectura de datos. El daño ocurre al abrir el archivo, sin ejecutar el modelo. Prefiere safetensors; si es pickle, weights_only=True y, aun así, desconfía.
  • Creer que un escáner que pasa = archivo seguro. picklescan es una lista negra que se evade (7z, gadgets nuevos). Pasar el escáner prueba ausencia de patrones conocidos, no ausencia de malicia.
  • Confundir “open weight” con “auditado y libre”. Abierto es descargable, no revisado ni sin restricciones de licencia. La model card es autodeclarada: información, no prueba.
  • Rangos de versión abiertos en dependencias. transformers>=4 acepta la versión que un atacante publique mañana. Pinear versión con hash es control de cadena de suministro, no manía de reproducibilidad.
  • SBOM que se detiene en el código. Si el inventario no lista modelos y datasets con su hash y fuente, el día del incidente no puedes responder “¿lo usamos y dónde?”.
  • Verificar el hash contra el mismo servidor de descarga. Si el atacante controla ambos, altera los dos y te miente. El hash de referencia viene de otro canal, idealmente firmado.
  • Cargar lo no confiable con permisos de producción. Un modelo dudoso se carga en un sandbox sin credenciales ni red, no en el proceso que tiene tus llaves de nube.

La frase para llevarse

“Un peso no es un archivo de datos: en el formato equivocado, cargarlo es ejecutar código. Descargar un modelo del Hub es confiar en un desconocido — la única pregunta es si esa confianza la verificaste o la asumiste.”

Qué viene

Módulo 10: sandboxing y ejecución de código. Este módulo terminó con la regla “lo no confiable se carga preso, no suelto”, pero no dijo cómo se construye la prisión. El próximo baja a lo concreto: cómo aislar la carga de un modelo dudoso y, sobre todo, cómo darle a un agente la capacidad de ejecutar código — la tool más poderosa y más peligrosa que existe — sin entregarle tu infraestructura. Contenedores, permisos mínimos, sin red de salida, sistemas de archivos efímeros: el arte de dejar que algo corra sin dejar que haga daño.

Material relacionado

SeguridadSupply ChainModelosDependenciasSBOM

Preguntas sobre este curso

Regístrate gratis para hacer preguntas y guardar tu progreso.

✉ Entérate de lo nuevo

Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.