Metaflow (Netflix): superficie grande, tesis honesta
El más cargado en Capa1 de la serie: 68 conocidos, 319 cuarentena, 228 match. Shell, pickle y vendors — mapa, no trophy wall.
Esto no es “hackeamos Netflix”. Es un scan sobre el paquete público
metaflow/. Muchos hits viven en vendors o bootstraps de entorno.
Candidato estático ≠ vulnerabilidad explotable confirmada.
Qué miramos: Metaflow (Netflix) — metaflow/, ≈418 archivos Python,
branch master (3d6fa453…), sin ejecutar el target.
Qué salió: 68 conocidos Capa1 (el más cargado de los tres), 319 cuarentena,
228 match, 91 nuevos. Oráculo 0/0.
Qué significa: un orquestador de ML toca shell, pickle, paths y vendors —
el scanner mapea esa superficie.
Qué NO significa: 68 CVEs nuevas ni que Metaflow esté “comprometido”.
Tabla de métricas
| Métrica | Valor |
|---|---|
| Repo / org | Netflix/metaflow · Netflix |
| Branch / SHA | master · 3d6fa453 |
| Scan path | metaflow/ · ≈418 .py · 9.7 s |
| Capa1 conocidos | 68 |
| Cuarentena / match / nuevos | 319 / 228 / 91 |
| Oráculo pass / fail | 0 / 0 |
| Top objetivos (match) | rce_eval 76 · mass_assignment 55 · cmd_injection 50 · ssti 10 |
| CWE Capa1 (top) | CWE-502 ×11 · CWE-916 ×8 · CWE-347 ×7 · CWE-22 ×6 · CWE-79 ×5 |
Auditoría de impacto (lo que sí huele fuerte)
Metaflow es orquestación: pickle entre procesos, bootstrap de entornos, plugins cloud. El threat model útil es artefacto / datastore / extension no confiable, no “internet anónimo pega al CLI”.
1. Pickle en multicore, env_escape y datastores — CWE-502 ×11
Ancla
multicore_utils.py:132/140; plugins/datastores/serializers/pickle_serializer.py:70; cluster denso en plugins/env_escape/data_transferer.py (116, 136, 151, 159, 167, 177, 186, 241).
Qué pasa
Serialización pickle para mover objetos entre workers / escape de entorno / datastore. Misma clase: RCE por deserialización si el bytes stream es adversario.
Qué se puede hacer / cómo podría fallar
Impacto: código en el runtime del flow (laptop, Batch, K8s). Precondiciones: escritura al canal (socket, archivo, S3 object, mensaje) que el peer deserializa sin autenticar/firmar. env_escape es frontera de confianza explícita — si el “otro lado” está comprometido, el diseño asume riesgo.
Cómo podría ser explotado (abstracto)
Cadena: objeto hostil en datastore compartido o man-in-the-middle en canal de escape → loads → gadget. No confirmado sin mapa de quién puede escribir esos blobs en un deploy típico. Oráculo=0 aquí es esperado.
2. Subprocess / os.system / shell — obj_cmd_injection
Ancla
Matches: os.system en _vendor/click/_termui_impl.py:340 (y vecino 417); subprocess.Popen sidecar; check_output en plugins/pypi/pip.py, cards; varios shell=True.
Qué pasa
Bootstraps PyPI/micromamba, sidecars y pager de Click lanzan el shell del SO. Superficie grande de command injection si paths, URLs o nombres de paquete no confiables entran al string.
Qué se puede hacer / cómo podría fallar
Impacto local al runner del flow. Precondiciones: control de argv/env que se interpola; en vendor Click, pager con filename — clásico si cmd/path son influenciables. Distinguir código Metaflow propio vs _vendor/.
Cómo podría ser explotado (abstracto)
Escenario: dependency/URL/path de bootstrap hostil → subprocess → shell. Checklist defensivo: lista de args vs shell string; pin de URLs; no tratar todo Popen como P0 sin taint.
3. Paths, tar/zip bomb, permisos — CWE-22 / 409 / 732
Ancla
Paths: tutorials_cmd.py:70/145, metaflow_config_funcs.py:84, plugins/pypi/bootstrap.py:310, airflow decorator. Decompresión: tar_backend.py:89, plugins/uv/bootstrap.py:65, util.py:560. Permisos amplios: airflow/argo/batch set_parameters, pypi bootstrap.
Qué pasa
Extracción de packs y escritura de params con mode amplio. Clases: path traversal, zip/tar bomb (DoS disco), insecure permissions (lectura de secretos por otros users del host).
Explotación abstracta
Artefacto de tutorial/bootstrap hostil → extract fuera de root o fill disk; o archivo de params world-readable con credenciales. Falta: política de umask y contención por call site.
4. Eval / carga dinámica — CWE-95 / 829
Ancla
stub_generator.py (139, 537, 670, 1008) eval; :391 carga de módulo; datastore/artifacts/serializer.py:730; user_configs/config_parameters.py:378.
Qué pasa
Tooling de stubs y configs de usuario ejecutan o importan según strings. Clase code/module injection si el string sale de un artefacto no confiable.
Explotación abstracta
Extension o config plantada → import/eval en el proceso del desarrollador o del runner. Precondición fuerte: trust del árbol de proyecto / plugins. Mucho CWE-347 “firma_no_verificada” en extension_support parece heurística de nombre, no ausencia de firma crypto — no lo trates como “JWT sin verify” sin leer el código.
5. SSRF / cards HTML / clickjacking
Ancla
CWE-918 argo_events.py:142; matches requests.get/urlopen (aws_client, batch metadata, pypi bootstrap); CWE-79 en packaging vendor; CWE-1021 headers en argo_workflows.py (3337+); chevron/pt.render en cards.
Qué pasa
Fetches a URLs de eventos/metadata/paquetes; cards renderizan HTML/Mustache. Clases: SSRF si URL es user-controlled; XSS en card UI si datos no escapan; clickjacking si se sirven páginas sin frame guards.
Nota
Varios requests.get van a IMDS/metadata de AWS — patrón cloud, no SSRF de atacante salvo que la URL se arme desde input. Separar sinks fijos vs variables.
Lo que no conocemos (banco / cuarentena sin etiqueta fina)
91 candidato_nuevo — el banco más gordo de la serie. Grupos:
Grupo A — platform.system (×11)
Fingerprint OS en packaging/pypi. Probable ruido. Solo escalaría si el string alimenta argv de micromamba/pip sin quote. Checklist: grep de flujo hacia subprocess.
Grupo B — *.decode / self.decode (click vendor, env_escape, s3)
Bytes→str en boundaries. En env_escape, self.decode está al lado del protocolo que también picklea — la pregunta útil no es decode sino quién autentica el mensaje. Checklist: confiar el canal antes que el codec.
Grupo C — ast.parse (×4) / VALID_NAME.search
ast.parse en typeguard vendorizado y graph.py:458: parsear AST no es ejecutar. Riesgo real sería compile+exec sobre fuente de usuario (hay matches compile en decorators). VALID_NAME es validador de nombres Argo/Step Functions — casi higiene, a veces matcheado a familias raras (obj_cors_reflect en agregado). Checklist: no promover parse→RCE sin exec.
Grupo D — re.search / render de cards
Validación de project name, LICENSE scan, card self.render. Sospecha ReDoS/SSTI genérica. Probable ruido salvo template string controlado en chevron. Checklist: origen del template vs datos del task.
Ruido vs señal
- Descartaría: CWE-79 en
_vendor/packaging__repr__— no es XSS de app. - Descartaría / degradar: oleada
re.compile→obj_rce_eval(64 re.compile en match sinks);setattr×50 mass_assignment. - Cuidado con CWE-916: muchos “hash sin costo” son digests de integridad (appspec/batch), no password storage — mala etiqueta semántica.
- Señal: pickle cluster (env_escape/datastore), subprocess en pypi/sidecar, path/tar en bootstrap, eval en stubs/config, urlopen argo_events.
- Límite: vendor ruidoso; oráculo 0; shallow.
Veredicto del auditor
- P0 Trust boundary de pickle: datastore + env_escape — quién puede meter bytes al canal.
- P1 Bootstrap pypi/uv/tar: subprocess args, path contención, bombas de decompress.
- P1 Separar vendor Click/packaging del código Metaflow en el reporte (hoy infla Capa1/Capa2).
- P2 Cards HTML/SSRF metadata, permisos 732, candidatos platform/ast/decode, hashes 916 mal etiquetados.
Metaflow es el más “cargado”, pero gran parte es vendor y fingerprints. El cerebro del auditor se va a pickle y bootstraps, no a re.compile.
Capa1 vs Capa2 / cuarentena
subprocess / shell=True en bootstraps y vendor click.
re.compile → obj_rce_eval en click vendorizado).
Detalle técnico — hallazgos del JSON
env_escape/data_transferer.py (varios sitios) y multicore_utils.py:132.
Misma lógica que Django cache: deserialización explícita — el motor la etiqueta; el riesgo depende del trust boundary.
_parser.py:21, markers.py:216, requirements.py:80…).
Tratar con escepticismo: firma HTML sobre librería de versiones — candidato a FP de firma, no XSS web clásico.
if subprocess.call(cmdline, shell=True):
raise ExternalCommandFailed(
"Command '%s' returned a non-zero exit code." % cmdline)
plugins/pypi/bootstrap.py:43 y plugins/uv/bootstrap.py:20 con
subprocess.run(..., shell=True).
bootstrap.py:172) en bootstraps de entorno — superficie de install/runtime.re.compile cayendo en familia eval — mismo tipo de FP que vimos en aws-cli/Django.Cómo se validaría bien (defensivo)
Separar vendors vs código propio; fixture que cubra el patrón (p. ej. wrapper con
shell=True en harness aislado); oráculo Capa2 + revisión humana del trust boundary
del runner. Nada de payloads, canaries ni contactar a terceros “para probar”.
Serie: caso agregado · Claude · aws-cli · Django · Metaflow · home