Go解析器中的意外安全陷阱与漏洞利用

本文深入分析Go语言JSON、XML和YAML解析器的安全陷阱,包括字段标签误用、解析器差异和数据格式混淆等漏洞,提供实际攻击案例和缓解方案。

Go解析器中的意外安全陷阱

在Go应用程序中,解析不可信数据会创建危险的攻击面,这些攻击面在野外经常被利用。在我们的安全评估过程中,我们反复利用Go的JSON、XML和YAML解析器中的意外行为来绕过身份验证、规避授权控制并从生产系统中窃取敏感数据。

这些不是理论问题——它们导致了有记录的漏洞,如CVE-2020-16250(Google的Project Zero发现的Hashicorp Vault身份验证绕过)以及我们在客户参与中的许多高影响发现。

本文通过三个攻击场景来阐述这些意外的解析器行为,每个安全工程师和Go开发人员都应该了解:

  1. (解)封送意外数据:Go解析器如何暴露开发人员意图保持私密的数据
  2. 解析器差异:当多个服务解析相同输入时,解析器之间的差异如何使攻击者能够绕过安全控制
  3. 数据格式混淆:解析器如何处理跨格式payload并产生令人惊讶且可利用的结果

我们将用真实世界的例子演示每个攻击场景,并以更安全地配置这些解析器的具体建议作为结束,包括补偿Go标准库中安全漏洞的策略。

解析器行为安全状态摘要

行为 JSON XML YAML
json:"-,... 🔴 不安全 🔴 不安全 🔴 不安全
json:"omitempty" 🟢 安全 🟢 安全 🟢 安全
重复键 🟠 可配置 🟠 可配置 🟢 安全
大小写不敏感 🔴 不安全 🟢 安全 🟢 安全
未知键 🟠 可配置 🟠 可配置 🟠 可配置
前导垃圾数据 🟢 安全 🔴 不安全 🟢 安全
尾随垃圾数据 🟠 可配置 🔴 不安全 🟢 安全

Go中的解析

  • encoding/json 版本 go1.24.1
  • encoding/xml 版本 go1.24.1
  • yaml.v3 版本 3.0.1(最流行的第三方Go YAML库)

这些解析器提供两个核心功能:

  • Marshal(序列化):将Go结构体转换为相应的格式字符串
  • Unmarshal(反序列化):将格式字符串转换回Go结构体

Go使用结构体字段标签来自定义解析器如何处理各个字段:

1
2
3
4
5
type User struct {
    Username string `json:"username_json_key,omitempty"`
    Password string `json:"password"`
    IsAdmin  bool   `json:"is_admin"`
}

攻击场景1:(解)封送意外数据

没有标签的字段

当字段没有JSON标签时,仍然可以使用字段名称进行解组:

1
2
3
type User struct {
    Username string
}

这可能导致安全漏洞,如果开发人员错误地认为没有标签的字段不会被解析。

误用-标签

要告诉解析器不要(解)封送特定字段,必须添加特殊的- JSON标签:

1
2
3
type User struct {
    IsAdmin bool `json:"-,omitempty"`  // 错误用法!
}

这种误用会导致解析器查找输入JSON中的-键,从而意外设置字段。

误用omitempty

开发人员有时错误地将字段名称设置为omitempty

1
2
3
type User struct {
    Username string `json:"omitempty"`  // 字段名变成了"omitempty"
}

攻击场景2:解析器差异

重复字段

当JSON输入有相同的键两次时,Go的JSON解析器总是取最后一个值:

1
2
_ = json.Unmarshal([]byte(`{"action": "Action1", "action": "Action2"}`), &a)
// 结果: ActionRequest{Action:"Action2"}

这种差异可被利用来绕过授权检查。

大小写不敏感键匹配

Go的JSON解析器以大小写不敏感的方式解析字段名称:

1
2
_ = json.Unmarshal([]byte(`{"aCtIoN": "Action2"}`), &a)
// 结果: ActionRequest{Action:"Action2"}

这种行为与大多数其他语言的解析器不同,导致了多个高影响的安全漏洞。

攻击场景3:数据格式混淆

未知键

默认情况下,JSON、XML和YAML解析器不阻止未知字段。

前导垃圾数据

只有XML解析器接受前导垃圾数据。

尾随垃圾数据

XML解析器接受任意尾随垃圾数据,JSON解析器在使用Decoder API时也接受尾随垃圾数据。

构建多格式payload

我们可以构建一个能被Go的JSON、XML和YAML解析器解析的多格式payload,每个解析器返回不同的结果:

1
2
3
4
5
{
  "Action": "Action_1",
  "action": "Action_2",
  "comment": "<!--<Action>Action_3</Action>-->"
}

缓解措施

JSON严格解析函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func strictJSONParse(jsonData []byte, target interface{}) error {
    decoder := json.NewDecoder(bytes.NewReader(jsonData))
    decoder.DisallowUnknownFields()
    
    // 检测大小写不敏感的键冲突
    err := DetectCaseInsensitiveKeyCollisions(jsonData)
    if err != nil {
        return err
    }
    
    // 解码JSON
    err = decoder.Decode(target)
    if err != nil {
        return err
    }
    
    // 确保没有尾随数据
    token, err := decoder.Token()
    if err != io.EOF {
        return fmt.Errorf("unexpected trailing data")
    }
    
    return nil
}

JSONv2

JSON v2提案解决了本文讨论的许多问题:

  • 禁止重复名称
  • 执行大小写敏感匹配
  • 包含RejectUnknownMembers选项
  • 包含UnmarshalRead函数来处理io.Reader数据

开发人员关键要点

  1. 默认实施严格解析:对JSON使用DisallowUnknownFields,对YAML使用KnownFields(true)
  2. 跨边界保持一致性:确保多个服务中的解析行为一致
  3. 关注JSON v2:关注Go的JSON v2库的开发
  4. 利用静态分析:使用Semgrep规则检测代码库中的漏洞模式

虽然我们提供了缓解和检测策略,但长期解决方案需要从根本上改变这些解析器的操作方式。在解析器库采用安全默认值之前,开发人员必须保持警惕。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计