CVE
CVE-2026-33264
CWE
CWE-502
Sistemas afectados
- Paquete PyPI `apache-airflow` antes de la `3.3.0` donde los autores de DAG tienen menor confianza que los procesos del Scheduler o del API Server
- Nodos de Scheduler y API Server de Airflow que cargan DAG serializados que contienen cargas útiles de trigger controladas por el atacante
- Despliegues que dependen de listas de permitidos de deserialización permisivas o de expresiones regulares amplias bajo `[core] allowed_deserialization_classes` o `[core] allowed_deserialization_classes_regexp`
CVE-2026-33264 es una de las cadenas de divulgación de Python más importantes de esta semana porque no es “solo otro” fallo de deserialización insegura en una biblioteca hoja. Cruza un límite de confianza real dentro de Airflow. A un autor de DAG a menudo se le permite definir flujos de trabajo, triggers y comportamiento de tareas sin ser equivalente a un administrador del scheduler o del servidor web. Este fallo rompió esa separación al deserializar estado de trigger controlado por el atacante mientras el Scheduler y el API Server cargaban DAG serializados.
El texto del propio proveedor es preciso: BaseSerialization.deserialize() podía realizar un import_string() sin restricciones sobre rutas de clase controladas por el atacante incrustadas en campos de trigger de DAG serializados. Si tu despliegue asume que el código de un autor de DAG no debe ejecutarse dentro del API Server o del Scheduler, esto es una ruta de compromiso del plano de control, no un problema cosmético del analizador.
La ruta vulnerable era trabajo innecesario en el proceso equivocado
Lo más útil de la corrección de Airflow es que muestra que el fallo existía en una ruta que nunca debería haber necesitado deserialización en absoluto.
Antes de la corrección, la carga de argumentos de start-trigger serializados se veía así:
def _decode_start_trigger_args(var: dict[str, Any]) -> StartTriggerArgs:
def deserialize_kwargs(key: str) -> Any:
if (val := var[key]) is None:
return None
return BaseSerialization.deserialize(val)
return StartTriggerArgs(
trigger_cls=var["trigger_cls"],
trigger_kwargs=deserialize_kwargs("trigger_kwargs"),
next_method=var["next_method"],
next_kwargs=deserialize_kwargs("next_kwargs"),
timeout=datetime.timedelta(seconds=var["timeout"]) if var["timeout"] else None,
)
Eso significa que una carga de DAG serializado en el Scheduler o el API Server podía tomar JSON controlado por el atacante, rehidratarlo en objetos Python e invocar exactamente la maquinaria de deserialización que se supone debe permanecer detrás de límites de confianza más estrictos.
La corrección es casi aburrida en el mejor sentido posible:
return StartTriggerArgs(
trigger_cls=var["trigger_cls"],
trigger_kwargs=var["trigger_kwargs"],
next_method=var["next_method"],
next_kwargs=var["next_kwargs"],
timeout=datetime.timedelta(seconds=var["timeout"]) if var["timeout"] else None,
)
En otras palabras: deja de deserializar esos campos ahí, directamente.
Esa corrección de diseño dice mucho sobre la causa raíz. El fallo no era simplemente “la deserialización es peligrosa”. Era que los procesos equivocados de Airflow estaban haciendo una deserialización que no necesitaban hacer.
Por qué import_string() convierte esto en un límite de RCE
El serializador de Airflow puede reconstruir objetos de Python más ricos que simples diccionarios y listas. En este caso, el aviso señala el import_string() sin restricciones de rutas de clase controladas por el atacante. Ese es el paso de escalada clave:
DAG serializado
-> _decode_start_trigger_args()
-> BaseSerialization.deserialize(...)
-> import_string("attacker.controlled.module.Class")
-> importación / instanciación de clase en el contexto del Scheduler o del API Server
Si los autores de DAG tienen menor confianza que esos procesos del plano de control, el límite de seguridad desaparece en ese punto.
Por eso la pregunta sobre el despliegue afectado importa más que la etiqueta genérica de CVSS. En algunos entornos de un solo inquilino, los mismos operadores pueden ser propietarios tanto del código de los DAG como del plano de control de Airflow. En muchos entornos compartidos u operados por un equipo de plataforma, no es así. Este último caso es donde el fallo es peligroso: permite que los autores de flujos de trabajo alcancen procesos que normalmente tienen un acceso más amplio a la base de datos, autoridad de scheduler o privilegios web/API.
La corrección preserva el comportamiento de los triggers manteniendo el JSON en crudo hasta el límite correcto
La descripción del PR es valiosa porque explica por qué eliminar la deserialización aquí no rompe el manejo legítimo de triggers. El Triggerer todavía lee los kwargs de trigger cifrados desde la fila de la base de datos de Trigger en lugar de depender de efectos secundarios de la deserialización de DAG. Por lo tanto, la corrección cambia quién toca los datos y cuándo, no solo la sintaxis de la carga útil.
Las pruebas añadidas en el parche lo hacen explícito. Tras la deserialización, trigger_kwargs ahora permanece codificado:
assert task.start_trigger_args.trigger_kwargs == {
"__type": "dict",
"__var": {"delta": {"__type": "timedelta", "__var": 2.0}},
}
assert task.start_trigger_args.next_kwargs == {
"__type": "dict",
"__var": {"resume_after": {"__type": "timedelta", "__var": 5.0}},
}
Eso es lo que quieres en el Scheduler o el API Server: datos opacos y serializados que no se convierten en objetos Python vivos hasta que un componente confiable realmente los necesita.
El parche también tuvo que normalizar correctamente las claves codificadas
Una parte sutil de la corrección es que, una vez que Airflow dejó de deserializar ávidamente estos campos, tuvo que serializar las claves marcadoras codificadas de forma consistente entre versiones de Python. El parche introdujo un ayudante para normalizar recursivamente las claves del enum Encoding:
def stringify_encoding_keys(d: Any) -> Any:
if isinstance(d, dict):
return {
(k.value if isinstance(k, Encoding) else str(k)): stringify_encoding_keys(v)
for k, v in d.items()
}
if isinstance(d, list):
return [stringify_encoding_keys(i) for i in d]
if isinstance(d, tuple):
converted = [stringify_encoding_keys(i) for i in d]
if hasattr(d, "_fields"):
return type(d)(*converted)
return tuple(converted)
return d
Eso importa porque el parche no fue simplemente “elimina una llamada a deserialize y ya está”. Airflow ya había acoplado partes del procesamiento de triggers al comportamiento antiguo. Una vez eliminada la deserialización innecesaria, el código tuvo que preservar una semántica consistente de claves marcadoras para que el Triggerer pudiera seguir reconstruyendo valores legítimos más adelante.
La lección de ingeniería es útil más allá de Airflow: una vez que la deserialización insegura se ha filtrado a la capa equivocada, eliminarla a menudo revela suposiciones ocultas en otras partes de la pila.
Airflow 3.3.0 también ajustó la semántica de la lista de permitidos basada en expresiones regulares
El lanzamiento de la 3.3.0 incluye otro cambio de endurecimiento adyacente: [core] allowed_deserialization_classes_regexp ahora usa la semántica de re.fullmatch() en lugar de re.match(). La documentación señala un ejemplo donde un patrón como:
airflow\.models\.Variable
previamente coincidía con nombres de clase no deseados que simplemente empezaban con ese prefijo, como:
airflow.models.Variable_Malicious
Ese punto de las notas de la versión no es la misma causa raíz que CVE-2026-33264, pero pertenece a la misma conversación operativa. Si ya estás usando listas de permitidos de clases de deserialización como límite de defensa en profundidad, un emparejamiento de prefijos descuidado debilita ese control. Airflow ahora exige coincidencias de cadena completa, y los operadores que dependen de expresiones regulares de estilo prefijo necesitan actualizarlas explícitamente con .* donde corresponda.
Qué está realmente afectado
Según los datos del aviso, el límite del paquete afectado es simple:
Paquete PyPI: apache-airflow
Versiones afectadas: < 3.3.0
Versión corregida: 3.3.0
La pregunta más difícil es arquitectónica:
- ¿Tienen los autores de DAG menor confianza que los operadores del Scheduler o del API Server?
- ¿Están habilitados los DAG serializados en nodos del plano de control que analizan estado de trigger controlado por el autor?
- ¿Permites clases personalizadas a través de la configuración de deserialización?
- ¿Has configurado expresiones regulares de deserialización permisivas escritas con suposiciones de emparejamiento por prefijo?
Si la respuesta a las dos primeras preguntas es sí, deberías tratar esto como un verdadero fallo de límite de privilegios.
Detección y validación
Empieza confirmando la versión y revisando la política de deserialización:
python3 -c "import airflow; print(airflow.__version__)"
pip show apache-airflow
rg -n \
"allowed_deserialization_classes|allowed_deserialization_classes_regexp" \
airflow.cfg config .env
Luego revisa si los autores de DAG de menor confianza pueden definir tareas con triggers o referencias personalizadas que recorran el estado de DAG serializado:
rg -n "StartTriggerArgs|start_from_trigger|trigger_kwargs|next_kwargs|DeadlineReference" dags plugins
Si tu entorno es multiinquilino u operado por una plataforma, responde también estas preguntas de delimitación:
¿Pueden los autores de DAG enviar código o estado serializado sin acceso separado al plano de control?
¿Tienen los nodos del Scheduler o del API Server credenciales más amplias que el código de tareas solo de worker?
¿Están configuradas clases o expresiones regulares de deserialización personalizadas?
Para la respuesta a incidentes, conserva:
- la versión exacta de Airflow
airflow.cfg- los artefactos de DAG serializados
- las definiciones de plugins y los registros de clases personalizadas
- el historial de auditoría de cambios recientes en DAG que involucren triggers o referencias personalizadas
Remediación
La corrección del proveedor es sencilla:
- actualiza a
apache-airflow3.3.0o posterior - restringe
[core] allowed_deserialization_classesal conjunto más estrecho posible - revisa
[core] allowed_deserialization_classes_regexpen busca de patrones que dependieran del antiguo comportamiento de emparejamiento por prefijo - reevalúa si los autores de DAG deberían tratarse como principales de menor confianza respecto a tu plano de control
Si operas un servicio de Airflow compartido, esta divulgación también es un recordatorio de política: “los autores de DAG ya pueden ejecutar código en algún lugar” no es lo mismo que “los autores de DAG deberían poder ejecutar código en el Scheduler o el API Server”.
Esa distinción es todo el fallo. El estado de trigger serializado de Airflow pasó de datos de flujo de trabajo controlados por el autor a deserialización del plano de control de mayor confianza. La corrección de la 3.3.0 funciona porque restaura una regla más simple: mantener esos campos como JSON en crudo hasta que el componente que legítimamente los necesita alcance el límite de ejecución correcto.
De la investigación a la remediación
Comprueba si este patrón existe en tu código
Convierte esta investigación en un flujo de remediación. Analiza dependencias y manifiestos para detectar riesgos similares en la cadena de suministro y prioriza las correcciones con contexto de alcanzabilidad.