3. 数据泄露场景一:指标(Metrics)标签泄露——错误地将租户ID暴露在公共标签中

3.1 场景描述

在多租户后端系统中,指标(Metrics)通常由 Prometheus、VictoriaMetrics 或 OpenTelemetry 等系统采集。当运维人员或开发者在定义指标标签(Label)时,无意中将租户 ID(tenant_id)作为公共标签附加到指标上,导致所有能够查询该指标的用户(包括其他租户)都能通过标签值枚举或推断出系统中存在的租户列表及其活动模式。

⚠️ 风险等级:高 — 租户 ID 属于敏感元数据,一旦暴露,攻击者可利用其发起租户枚举攻击、业务量分析或定向渗透。

3.2 泄露原理

指标系统通常允许用户通过标签进行聚合、过滤和分组。如果租户 ID 被设置为标签,则任何拥有指标查询权限的实体都可以执行类似 count by (tenant_id) 的查询,从而获取所有租户的 ID 列表。

组件 正常行为 泄露行为
指标定义 使用内部标签(如 instance、job) 将 tenant_id 作为公开标签
查询接口 只能看到自身租户的数据 可通过标签查询枚举所有租户
数据存储 租户数据物理或逻辑隔离 所有租户指标混存,标签成为唯一隔离手段

3.3 典型泄露路径

  1. 应用代码直接暴露:在业务逻辑中,将请求上下文中的 tenant_id 直接作为 Prometheus 指标标签。
  2. 中间件自动注入:API 网关或服务网格自动将请求头中的租户信息注入到指标中。
  3. 日志转指标:从日志中提取租户 ID 并转换为指标标签,未做脱敏处理。
  4. 自定义 Exporter:开发者在编写自定义 Exporter 时,为了方便调试,将租户 ID 作为标签输出。
🔍 排查要点: 检查所有自定义指标的定义代码,搜索 tenant_id、customer_id、org_id 等字段是否出现在 Labels() 或 WithLabelValues() 调用中。

3.4 泄露示例(代码层面)

以下是一个典型的错误实现,将租户 ID 直接暴露在指标标签中:

// ❌ 错误示例:将 tenant_id 作为公共标签
var requestCounter = promauto.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total number of HTTP requests",
    },
    []string{"method", "endpoint", "tenant_id"},  // tenant_id 暴露
)

func handler(w http.ResponseWriter, r *http.Request) {
    tenantID := r.Header.Get("X-Tenant-ID")
    requestCounter.WithLabelValues(r.Method, r.URL.Path, tenantID).Inc()
    // ...
}

正确的做法是将租户 ID 从标签中移除,或使用内部标签系统(如通过指标名称前缀隔离):

// ✅ 正确示例:使用指标名称前缀隔离租户
var requestCounter = promauto.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total number of HTTP requests",
    },
    []string{"method", "endpoint"},  // 不包含 tenant_id
)

func handler(w http.ResponseWriter, r *http.Request) {
    // 租户隔离通过指标命名空间或查询时过滤实现
    requestCounter.WithLabelValues(r.Method, r.URL.Path).Inc()
}

3.5 攻击者视角:如何利用泄露的租户 ID

  • 租户枚举:通过 count by (tenant_id) 获取所有活跃租户 ID。
  • 业务量分析:按租户聚合请求量、错误率,推断竞争对手或高价值客户。
  • 定向攻击:结合其他泄露信息,对特定租户发起 DDoS 或漏洞利用。
  • 合规风险:在金融、医疗等场景下,租户 ID 暴露可能违反 GDPR、HIPAA 等法规。

3.6 隔离方案对比

方案 隔离粒度 标签泄露风险 运维复杂度
标签隔离(错误) 粗粒度 高 低
指标名称前缀隔离 中粒度 中 中
独立指标实例(每个租户一个端点) 细粒度 低 高
查询时过滤(结合认证) 细粒度 低 高

3.7 架构视角:标签泄露的传播路径

下图展示了租户 ID 从应用代码泄露到指标系统的完整路径:

租户 ID 标签泄露传播路径 应用服务 代码中注入 tenant_id 指标采集器 Prometheus / OTEL 指标存储 VictoriaMetrics /metrics 端点 远程写入 租户 A 用户 查询自身指标 恶意用户 枚举所有 tenant_id 查询(仅限自身租户) 查询(可枚举所有租户) ⚠️ 关键风险点: 由于 tenant_id 作为公共标签存储在指标系统中,恶意用户可以通过 PromQL 查询 count by (tenant_id) 获取所有租户 ID。

3.8 检测与修复

阶段 检测方法 修复措施
代码审查 搜索所有指标定义中的标签列表,检查是否包含租户相关字段 移除租户 ID 标签,改用指标名称前缀或内部标签
运行时监控 对指标系统执行 count by (tenant_id) 查询,检查是否返回多个租户 配置指标采集器进行标签白名单过滤
安全扫描 使用 SAST 工具扫描代码中的指标定义 建立指标标签规范,禁止敏感字段作为标签
权限审计 检查指标查询接口的访问控制策略 实施租户级别的查询权限隔离
💡 最佳实践: 在指标系统中,永远不要将租户 ID、用户 ID、邮箱等个人身份信息(PII)作为标签。如果需要按租户隔离,应使用指标名称前缀(如 tenant_abc_http_requests_total)或部署独立的指标实例。

3.9 总结

指标标签泄露是最容易被忽视的数据泄露场景之一,因为开发者往往只关注指标的功能正确性,而忽略了标签的可见性。租户 ID 一旦作为公共标签进入指标系统,就会成为攻击者枚举租户、分析业务模式的突破口。修复的核心原则是:标签只用于描述技术维度(如 method、endpoint、status_code),不用于标识业务实体(如 tenant_id、user_id)。