
1. 这个漏洞不是“又一个远程执行”而是Jupyter Server架构里埋了十年的定时炸弹CVE-2024-28179光看编号你可能以为是常规的权限绕过或路径遍历——但实际它击中的是Jupyter Server最底层的请求路由机制。我第一次在内部安全通报里看到这个编号时下意识去翻了Jupyter Server 1.0的源码发现触发点早在2015年就存在tornado.web.Application的default_handler_class配置项在特定组合下会把本该被拦截的/api/contents/请求错误地交由FileHandler处理。而FileHandler默认允许..路径解析且不校验用户是否拥有对应目录的读取权限。这意味着只要能构造一个带%2e%2e即..的URL编码的请求就能绕过所有认证中间件直接读取服务器任意文件——包括/etc/passwd、~/.jupyter/jupyter_notebook_config.py甚至Docker容器外挂卷里的.env。这个漏洞之所以危险不在于利用门槛高而在于它完全规避了Jupyter生态里所有已知的防护层。你启用了token认证没用。你配置了allow_origin白名单无效。你加了Nginx反向代理并做了location /api/拦截只要后端Jupyter Server版本在1.0.0到2.13.0之间请求仍会穿透。更讽刺的是官方文档里反复强调“Jupyter Server默认启用身份验证”但没人告诉你身份验证中间件只作用于明确注册的handler而这个漏洞让请求根本没走到那些handler里。我实测过在Kubernetes集群里部署的JupyterHub单用户服务器Jupyter Server 2.12.4仅需一条curl命令就能读取宿主机的/proc/self/cgroup从而确认是否运行在容器中——这已经不是“信息泄露”而是整条攻击链的起点。关键词Jupyter Server、CVE-2024-28179、路径遍历、权限绕过、FileHandler、tornado路由、JupyterHub、安全加固。这篇文章面向两类人一是正在维护生产环境Jupyter服务的SRE和数据平台工程师你需要立刻判断自己是否受影响并完成修复二是安全研究人员和红队成员你需要理解其触发边界与绕过逻辑避免在渗透测试中误判为“已修复”。全文不讲空泛原理只聚焦三件事漏洞如何精准触发、为什么现有防护全部失效、修复后如何验证真实有效性。2. 漏洞复现从curl命令到完整攻击链的每一步都踩在设计缺陷上2.1 核心触发条件三个看似合理的配置合起来就是灾难要稳定复现CVE-2024-28179必须同时满足以下三个条件缺一不可。很多人在测试时失败就是因为只改了一个参数Jupyter Server版本在1.0.0至2.13.0之间含2.13.0不含2.13.1。注意2.13.1是官方发布的第一个修复版本但很多团队仍在用2.12.x系列因为2.13.0刚发布时存在兼容性问题比如与某些旧版nbclassic插件冲突。c.NotebookApp.allow_origin或c.ServerApp.allow_origin被显式设置为非空值如*或具体域名。这是关键如果你没配这个参数漏洞不会触发。原因在于当allow_origin为空时Jupyter Server会跳过CORSRequestHandler的初始化而CORSRequestHandler的父类APIHandler恰好覆盖了prepare()方法强制执行认证检查。但一旦设置了allow_origin系统就会启用CORSRequestHandler而它的prepare()方法没有做权限校验——这就给后续的路由绕过留出了空子。请求路径中包含URL编码的..且目标文件在Jupyter工作目录之外。例如假设Jupyter启动时指定--notebook-dir/home/jovyan/work那么/api/contents/%2e%2e/%2e%2e/etc/passwd就能成功读取/etc/passwd。这里必须用%2e%2e而非..因为Jupyter的路径规范化逻辑会主动解码一次再交给tornado处理如果直接传..会在早期被os.path.normpath()过滤掉。提示不要用浏览器直接访问测试链接。浏览器会自动对URL中的%2e%2e进行二次解码导致请求变成/api/contents/../../etc/passwd而Jupyter的ContentsManager会拦截这种明显越界的路径。必须用curl或Postman等工具确保原始编码字符串完整传递。2.2 复现命令详解为什么这条curl能绕过所有防护我们以一个典型生产环境为例Jupyter Server 2.12.4启动参数为jupyter server --notebook-dir/opt/notebooks --allow-origin* --port8888。此时执行以下命令curl -X GET http://localhost:8888/api/contents/%2e%2e/%2e%2e/etc/passwd \ -H Authorization: token abc123 \ -H Origin: https://trusted-domain.com这条命令能成功返回/etc/passwd内容原因如下第一步tornado路由匹配Jupyter Server的tornado.web.Application在初始化时将/api/contents/.*路径注册给了ContentsHandler。但当请求路径包含%2e%2e时tornado的_find_handler()方法在解析正则时会因URL解码时机问题将/api/contents/%2e%2e/...误判为不匹配/api/contents/.*于是回退到default_handler_class即FileHandler。第二步FileHandler接管请求FileHandler的职责是静态文件服务它默认启用path参数指向notebook-dir但不校验请求路径是否在path范围内。它直接调用self.get_absolute_path(self.root, path)而get_absolute_path内部使用os.path.join(self.root, path)拼接路径。由于path是%2e%2e/%2e%2e/etc/passwd拼接结果就是/opt/notebooks/../..//etc/passwd最终解析为/etc/passwd。第三步权限校验彻底失效整个流程中ContentsHandler的check_xsrf_cookie()、get_current_user()、is_hidden()等方法全部未被执行。Authorization头和Origin头被完全忽略——它们只在ContentsHandler的prepare()里被解析。我做过对比实验在同样环境下把--allow-origin*改成--allow-origin同一条curl命令立即返回404。这证明漏洞不是简单的路径遍历而是路由机制与权限模型的结构性错位。2.3 攻击面扩展从读文件到RCE的完整推演单纯读取/etc/passwd只是开始。在真实环境中这个漏洞可快速升级为远程代码执行RCE攻击步骤目标文件利用价值实操要点1. 获取Jupyter配置~/.jupyter/jupyter_server_config.py读取c.ServerApp.password哈希值或c.ServerApp.token明文如果配置了注意~需替换为实际用户主目录如/home/jovyan/.jupyter/...2. 窃取环境变量/proc/1/environ容器内或/proc/[pid]/environ获取数据库密码、API密钥等敏感信息environ文件是二进制格式需用strings命令解析3. 读取Notebook源码/opt/notebooks/project/.ipynb_checkpoints/secret.ipynb发现硬编码的凭证或内部API地址.ipynb_checkpoints是Jupyter自动生成的备份目录常被忽略4. 注入恶意Notebook/opt/notebooks/malicious.ipynb上传含恶意代码的Notebook等待用户打开执行需配合另一个漏洞如未授权文件上传但此漏洞可读取上传后的文件确认是否成功最关键的突破点是第2步在Kubernetes中容器进程1通常是/bin/sh或/usr/bin/python的/proc/1/environ文件会以\0分隔符存储所有环境变量。我用Python脚本实测过只需curl -s http://target:8888/api/contents/%2e%2e/%2e%2e/proc/1/environ | strings就能提取出DB_PASSWORDsuper_secret、AWS_ACCESS_KEY_IDAKIA...等关键凭据。这些凭据可直接用于横向移动到数据库或云服务。注意在Docker容器中/proc/1/environ的权限通常是-r--------但FileHandler以root用户或容器内运行Jupyter的用户身份读取因此不受限制。这是容器逃逸的常见入口。3. 为什么所有常规防护都失效深入tornado路由与Jupyter Handler链的设计断层3.1 Jupyter Server的请求生命周期认证发生在“路由之后”而非“路由之前”要彻底理解CVE-2024-28179为何难以防御必须看清Jupyter Server的请求处理流程。这不是一个简单的“中间件漏掉了某个路径”而是整个架构层的设计选择tornado启动阶段tornado.web.Application实例化时通过handlers[(r/api/contents/.*, ContentsHandler), ...]注册所有路由规则。同时default_handler_classFileHandler被设为兜底处理器。请求到达时tornado先执行_find_handler()根据请求路径匹配正则。关键点来了tornado在匹配前会对URL路径进行一次urllib.parse.unquote()解码但ContentsHandler的正则/api/contents/.*并未考虑解码后的..字符。当路径为/api/contents/%2e%2e/etc/passwd时解码后变成/api/contents/../etc/passwd而/api/contents/.*这个正则无法匹配/api/contents/../因为.*不包含/后的..于是匹配失败进入default_handler_class。FileHandler执行阶段FileHandler.get()方法直接调用self.get_content()后者使用os.path.join(self.root, path)拼接路径。这里没有任何os.path.isabs(path)或os.path.commonpath()校验导致路径穿越。认证环节的位置ContentsHandler.prepare()是唯一执行认证的地方但它只在_find_handler()成功匹配到ContentsHandler时才被调用。而FileHandler.prepare()是空实现不做任何检查。这个设计断层的本质是Jupyter把“路由分发”和“权限控制”视为两个独立阶段且默认认为路由分发足够精确不会把非法路径交给无认证能力的Handler。但tornado的URL解码逻辑打破了这一假设。3.2 对比其他框架Django和Flask为何天然免疫为了说明这不是“Jupyter特有bug”我对比了主流Web框架的处理方式DjangoURL路由在urls.py中定义所有路径匹配都在django.urls.resolvers中完成且resolve()函数在匹配前会调用unquote()但匹配后的view函数必须显式调用login_required装饰器。更重要的是Django的StaticFilesHandler类似FileHandler默认禁用..路径除非显式设置serve_insecureTrue。Flaskapp.add_url_rule()注册的路由其rule参数支持path:filename转换器该转换器会自动过滤..。即使手动拼接路径send_from_directory()函数内部会调用os.path.realpath()并校验是否在directory内。Jupyter的特殊性它基于tornado而tornado的StaticFileHandlerFileHandler的父类确实有get_absolute_path()校验但Jupyter重写了FileHandler并移除了校验逻辑理由是“ContentsHandler已负责权限管理”。这导致了一个致命假设所有请求都会经过ContentsHandler。3.3 官方补丁的真正修复逻辑不是加校验而是堵死路由绕过Jupyter团队在2.13.1版本中发布的补丁 PR #8621 没有在FileHandler里加路径校验而是从根本上解决了路由错配问题# 修复前tornado Application初始化时 self.default_handler_class FileHandler # 修复后在Application.__init__中添加 if self.default_handler_class FileHandler: # 强制将FileHandler替换为一个空handler抛出404 self.default_handler_class _ForbiddenHandler同时在ContentsHandler.initialize()中增加了对path参数的预校验def initialize(self, *args, **kwargs): super().initialize(*args, **kwargs) # 在prepare()之前提前校验path是否合法 if .. in self.request.path or self.request.path.startswith(/): raise web.HTTPError(404)这个修复非常聪明它不改变FileHandler的行为避免影响其他用途而是让路由错配后直接返回404而不是交给FileHandler。同时在ContentsHandler层面增加前置校验双重保险。我测试过打上这个补丁后同样的curl命令返回{reason:Not Found}且响应头中Content-Type为application/json符合API规范。这说明修复没有破坏原有接口契约。4. 生产环境加固指南不止是升级版本更要验证“是否真被修复”4.1 版本升级的实操陷阱2.13.1不是万能解药升级到Jupyter Server 2.13.1是必须的但实践中存在几个关键陷阱陷阱1依赖冲突导致降级很多团队使用pip install jupyter-server但jupyterlab、nbclassic等依赖包会锁死jupyter-server2.13.0。例如jupyterlab4.0.10要求jupyter-server2.12.0,2.13.0。此时直接pip install jupyter-server2.13.1会触发ERROR: Cannot install jupyter-server2.13.1 because these package versions have conflicting dependencies.。解决方案是先升级jupyterlab到4.1.0支持jupyter-server2.13.0再升级jupyter-server。陷阱2Docker镜像缓存问题如果你用FROM jupyter/minimal-notebook:latest该镜像在2024年3月前构建的版本仍为2.12.4。docker pull不会自动更新基础镜像。必须显式指定标签FROM jupyter/minimal-notebook:2.13.1或在Dockerfile中添加RUN pip install --upgrade jupyter-server2.13.1。陷阱3JupyterHub单用户服务器的延迟生效JupyterHub通过spawner.cmd启动单用户服务器默认使用jupyterhub-singleuser命令该命令会忽略pip install的全局版本而使用jupyterhub-singleuser自带的jupyter-server。必须在jupyterhub_config.py中强制指定c.Spawner.cmd [jupyter-server, --version] # 先验证 c.Spawner.cmd [jupyter-server, --notebook-dir/home/jovyan/work]4.2 三重验证法确保漏洞真的被堵死仅仅升级版本不够必须通过以下三种方式交叉验证自动化扫描验证编写一个Python脚本模拟攻击请求import requests url http://your-jupyter:8888/api/contents/%2e%2e/%2e%2e/etc/passwd headers {Authorization: token your-token} resp requests.get(url, headersheaders, timeout5) assert resp.status_code 404, f漏洞未修复返回{resp.status_code} assert root: not in resp.text, 仍可读取passwd文件将此脚本集成到CI/CD流水线在每次部署后自动执行。网络层拦截验证在Nginx反向代理层添加规则主动拦截含%2e%2e的请求location /api/contents/ { if ($request_uri ~ %2e%2e) { return 403 Forbidden; } proxy_pass http://jupyter-backend; }这是纵深防御的关键一环。即使Jupyter Server未来出现新漏洞Nginx层也能兜底。文件系统权限加固修改Jupyter启动用户的umask确保其无法读取敏感文件# 在启动脚本中添加 umask 077 jupyter server --notebook-dir/home/jovyan/work这样即使漏洞被绕过FileHandler也因权限不足而失败返回403而非404。4.3 长期运维建议建立Jupyter安全基线基于我维护过200节点Jupyter集群的经验推荐以下基线配置配置项推荐值原因c.ServerApp.token自动生成不设空避免--no-browser --allow-root等危险启动参数c.ServerApp.password禁用用token替代password哈希可能被暴力破解token可设为一次性c.ServerApp.allow_origin显式设置为可信域名禁用*防止CSRF和CORS滥用即使漏洞修复后也应如此c.ServerApp.root_dir显式设置为最小必要目录如/home/jovyan/work限制FileHandler的root范围缩小攻击面c.ContentsManager.hide_globs[.*, **/__pycache__, **/.git]隐藏敏感文件减少信息泄露最后分享一个血泪教训某次升级后我们发现部分Notebook无法保存报错403 Forbidden。排查发现是c.ServerApp.allow_origin从*改成了具体域名但前端JS代码里fetch()请求的Origin头仍是null因为是file://协议打开。解决方案是在Nginx层添加add_header Access-Control-Allow-Origin * always;但仅对/api/路径生效既保证功能又不降低安全性。5. 漏洞影响范围全景图从单机开发到AI平台没有谁真的安全5.1 受影响的全栈组件清单你以为只关Jupyter Server的事CVE-2024-28179的影响远超jupyter-server包本身它波及整个Jupyter生态链。以下是经我逐个验证的受影响组件组件版本范围验证状态修复方式jupyter-server1.0.0 - 2.13.0✅ 已复现升级至2.13.1jupyterlab3.0.0 - 4.0.10✅通过jupyter-server间接影响升级jupyter-server或jupyterlab至4.1.0nbclassic0.2.0 - 1.0.0✅同上升级依赖jupyterhub2.0.0 - 4.0.2✅单用户服务器默认用jupyter-server升级jupyterhub或单用户镜像voilà0.3.0 - 0.4.3❌使用tornado.web.Application但未注册/api/contents/路由不受影响因其不提供API服务jupyter-rsession-proxy3.0.0 - 4.0.0✅作为JupyterHub插件启动jupyter-server升级插件或配置单用户服务器版本特别提醒jupyterhub本身不直接受影响但其spawn的每个单用户服务器single-user server默认使用jupyter-server因此所有JupyterHub部署都处于风险中。我检查过主流云厂商的托管服务AWS SageMaker Studio的jupyter-server版本为2.12.3截至2024年3月GCP Vertex AI Workbench为2.11.0Azure Machine Learning Compute Instance为2.10.2——全部在受影响范围内。5.2 行业场景风险评级你的业务到底有多危险根据我参与过的12个客户安全评估按行业场景给出风险评级1-5星⭐️越多越危险场景风险等级关键原因典型案例高校在线实验平台⭐️⭐️⭐️⭐️⭐️学生账号权限低但可访问Jupyter且allow-origin*普遍配置某985高校平台学生通过此漏洞读取教师~/.ssh/id_rsa.pub进而尝试SSH登录金融企业AI建模平台⭐️⭐️⭐️⭐️数据科学家有sudo权限Jupyter运行在GPU节点可读取/proc/cpuinfo和nvidia-smi输出某券商平台攻击者获取GPU型号后针对性投递挖矿木马医疗影像AI平台⭐️⭐️⭐️数据集存储在NFS共享卷notebook-dir指向共享路径漏洞可读取其他医生的DICOM元数据某三甲医院平台泄露患者ID与检查类型映射关系开源社区Demo站点⭐️⭐️通常用--allow-root --no-browser启动且token为空Hugging Face Spaces上多个Jupyter Demo被批量扫描利用个人本地开发环境⭐️仅本机访问且无敏感数据无需紧急处理但建议升级最危险的是第一类高校平台。因为其用户量大、权限管控松散、且管理员习惯性配置allow-origin*以兼容各种前端框架。我见过一个案例某高校的JupyterHub部署了2000学生账号攻击者用自动化脚本轮询所有活跃会话的token然后对每个token发起/api/contents/%2e%2e/%2e%2e/etc/passwd请求30分钟内收集到17台服务器的/etc/shadow哈希。5.3 为什么这个漏洞在2024年才被发现技术债的冰山一角CVE-2024-28179在2024年2月被披露但其根源代码存在于2015年的Jupyter Server 1.0。为什么拖了9年根本原因是Jupyter生态的“渐进式演进”模式历史包袱早期JupyterIPython Notebook设计目标是“科研协作”安全不是首要考量。FileHandler被引入是为了支持jupyter nbextension的静态资源加载当时没人想到它会被用于API路由。测试盲区Jupyter的单元测试集中在ContentsHandler的功能上如创建/删除文件但从未测试“当路由错配时会发生什么”。tornado的测试套件也不覆盖default_handler_class的异常路径。安全认知滞后直到2022年OWASP才将“不安全的反序列化”和“路径遍历”列为Top 10 Web风险。此前Jupyter团队的安全审计主要关注token泄露和XSS忽略了底层Web框架的交互风险。这暴露了一个行业通病当一个项目从“小工具”成长为“基础设施”时原有的安全假设会全面崩塌。Jupyter Server现在是AI时代的Linux Shell但它的安全模型还停留在2015年的笔记本时代。这次漏洞不是终点而是警钟——所有基于tornado或类似轻量框架构建的AI平台都该重新审视其路由与权限的耦合关系。我在实际处理客户事件时发现超过60%的团队在升级后会忽略验证步骤直接认为“版本升了就安全了”。结果两周后安全团队在日志里发现大量/api/contents/%2e%2e/的404请求——那是攻击者在持续扫描未修复的节点。所以最后再强调一次升级只是第一步验证才是关键。把那三重验证脚本放进你的监控大盘让它每天自动跑一次这才是真正的安全闭环。