关于 secret-serialization
用于代码审查的 Skill,检查自动序列化与整体对象日志、遥测等环节结合时造成的间接凭据泄露。它指导审查者在整个代码库中追踪两端,包括当前改动之外的路径。
主要功能
- 01检查凭据字段及序列化器
- 02追踪整体对象日志与遥测出口
- 03区分生成路径与原始访问
- 04规定报告条件和严重程度
- 05提供具体修复指引
适用场景
- 01审查新增凭据字段的数据类型
- 02追踪日志或遥测中的对象序列化路径
- 03判断字段排除是否覆盖所有生成序列化器
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ getsentry/ skills --skill secret-serialization安装后在 Claude Code 中输入/secret-serialization确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
SOURCES.mdSPEC.mdreferences/javascript-typescript.mdpython.md详细介绍
密钥序列化审查
查找因类型会自行序列化,而上游又会序列化整个对象而导致泄露的凭据。
泄露通常需要两个分别看来都安全的改动:
- 持有方:凭据被存储为某个类型的字段,而该类型会生成
repr、str、asdict、model_dump、toJSON或属性枚举。 - 接收方:日志记录、追踪、错误报告、缓存或响应会序列化整个对象或每个 kwargs(
str(value)、span.set_data(key, obj)、logger.info("%s", obj)、JSON.stringify(args))。
这两部分往往由不同作者相隔数月编写。无论哪一侧单独出现,都应报告。始终在现有代码树中搜索另一侧,因为它经常已经存在于默认分支中,而不在差异中。
范围
- 本技能涵盖通过生成式序列化和整体接收端造成的间接泄露。
- 直接泄露(例如将令牌插入日志消息)属于
security-review。只有当它们来自改动后的整体接收端时,才在此处报告。 - 硬编码的密钥字面值和已提交的
.env文件不在范围内。
参考资料
| 参考资料 | 阅读时机 |
|---|---|
references/python.md | 审查 Python dataclass、attrs、pydantic、NamedTuple、msgspec、日志记录、Sentry SDK 或 OpenTelemetry 代码时 |
references/javascript-typescript.md | 审查 JavaScript 或 TypeScript 类、配置对象、JSON.stringify、util.inspect、pino 或 winston,或 Sentry 和 OpenTelemetry span 时 |
凭据字段
当字段的名称、类型或来源表明其包含凭据时,应将其视为凭据字段:
- 名称包含
secret、token、password、passwd、pwd、api_key、apikey、access_key、private_key、signing_key、client_secret、bearer、credential、authorization、auth_header、cookie、session_key、dsn、connection_string、webhook_secret、hmac或refresh_token。 - 类型为
SecretStr、SecretBytes或其包装类型。 - 值来自环境变量、密钥管理器,或名称类似上述字段的构造函数参数。
明确排除
区分两类路径:
- 生成路径由类型本身产生:
__repr__和__str__、asdict、model_dump、toJSON,以及遍历自有可枚举属性(JSON.stringify、util.inspect、展开运算符)。这些路径由持有者负责。 - 原始属性访问会直接读取存储的值:
vars(obj)、obj.__dict__、pickle、json.dumps(obj, default=vars)。无论字段具有什么标志,每个已存储的属性都会以这种方式暴露。只有当仓库中的某个接收点对包含凭证的实例应用此类访问时,才将其视为接收点并报告。
如果某个字段类型的所有生成路径都被一种或多种机制阻断,则该字段已完全排除。如果至少有一条生成路径仍包含该字段,则该字段仅部分排除。任何字段级机制都无法阻止原始属性访问。只有不存储该值才能做到这一点,因此应在接收点修复原始属性访问问题。
| 机制 | 阻断 | 仍包含该字段 |
|---|---|---|
dataclasses.field(repr=False)、attrs.field(repr=False) | __repr__、__str__ | asdict、astuple、原始访问 |
Pydantic Field(repr=False) | __repr__、__str__ | model_dump、model_dump_json、响应序列化、原始访问 |
Pydantic Field(exclude=True) | model_dump、model_dump_json、响应序列化 | __repr__、__str__、原始访问 |
Pydantic Field(repr=False, exclude=True) | 所有生成路径 | 原始访问 |
JavaScript #private 字段 | 所有生成路径 | 类主体之外的任何位置 |
Object.defineProperty(..., { enumerable: false }) | 所有生成路径 | 显式属性读取、Object.getOwnPropertyNames |
手写的 __repr__、__str__、toJSON 或 [util.inspect.custom],且省略该字段 | 仅覆盖该字段的路径(如果没有 __str__,则 __repr__ 也用于 str()) | 所有其他生成路径、原始访问 |
脱敏包装器:SecretStr、SecretBytes、项目自定义的 Redacted[T] | 所有生成路径 | pickle 和递归遍历 __dict__(例如 default=vars),这类操作会访问包装器内部存储的值 |
| 未存储在对象上:在方法内部读取,或保存在闭包中 | 所有路径 | 无 |
不要将下划线命名、TypeScript private、__slots__、类型注解、注释、dataclass(init=False),或仍然为该字段赋值的自定义 __init__,视为可以阻断任何路径的机制。
调查流程
- 阅读变更的代码块。找出自动序列化类型上新增或修改的凭证字段,以及新增或修改的代码中将对象或 kwargs 序列化到日志、span、错误上下文、缓存、队列或响应的部分。
- 对每个凭证字段,阅读完整的类、装饰器、基类和模型配置。列出所有生成路径,并根据上表检查该字段的机制会阻断哪些路径。
- 使用 grep 查找实例的构造位置和传递位置。传给装饰器、
**kwargs、工具调用参数、任务负载或中间件的实例,都可能到达通用接收点。 - 使用 grep 在整个仓库中查找能访问这些实例的接收点,而不只是变更差异中的部分。可搜索的模式包括:
str(value)、repr(、asdict(、model_dump(、set_data(、set_attribute(、set_context(、set_extra(、JSON.stringify(、util.inspect(、接收对象的日志记录器,以及原始属性访问(vars(、__dict__、pickle.dumps(、default=vars)。 - 对每个变更的接收点,检查是否存在键白名单、基于凭证名称的脱敏机制,或将对象替换为类型名称的过滤器。然后使用 grep 查找将包含凭证的对象传入该接收点的调用方。
- 查看测试时,只需确认是否有测试断言序列化结果中不包含凭证。缺少测试只能作为修复建议,不能作为问题报告。
- 确认代码会随产品发布。跳过测试夹具、示例、生成代码和仅用于测试的辅助工具。
报告内容
| 类别 | 报告条件 |
|---|---|
| 未排除的凭据字段 | 添加凭据字段,或将其移至启用了生成式序列化的类型中,且没有任何机制阻止其生成路径。 |
| 部分排除 | 至少有一条生成路径仍包含该字段,且仓库中的某个接收端会在该类型的实例上使用这条路径。 |
| 原始属性接收端 | 接收端对存储凭据的实例应用 vars、__dict__、pickle 或 default=vars,即使该字段已完全排除或经过包装也算。 |
| 凭据移至持有对象 | 重构将凭据从惰性读取或闭包移至对象字段。 |
| 全量序列化接收端 | 新增或更改遥测、日志、错误上下文、缓存或响应代码,使其序列化所有关键字参数、所有属性或整个对象,且没有允许列表或脱敏处理。 |
| 接收端失去过滤条件 | 更改移除或弱化了现有接收端中的脱敏、允许列表或将对象替换为类型名称的处理。 |
| 对象传入遥测 | 将客户端、配置或设置对象传入会将其字符串化的 span、日志或错误上下文调用。 |
严重程度
| 级别 | 适用情形 |
|---|---|
| high | 凭据到达仓库任意位置的接收端:日志行、span 属性、错误事件、缓存条目、持久化行、队列负载或 API 响应。另一端的代码可能早于本次 diff。也适用于新增的全量序列化接收端,且现有调用方会传入含凭据的对象。 |
| medium | 未排除的凭据字段,其实例会离开创建它的模块;仓库搜索未发现接收端。也适用于没有允许列表且未追踪到含凭据调用方的全量序列化接收端。 |
| low | 未排除的凭据字段,其实例永远不会离开创建它的作用域。 |
- 不要仅因为接收端或持有对象位于 diff 之外而降低严重程度。这正是此类漏洞的常见形式。
- 如果可达性取决于未经证实的前置条件,请选择较低的严重程度。
不要报告的内容
- 已完全排除的字段,除非实例传入了原始属性接收端。
- 部分排除的字段,除非仓库中有接收端使用未被阻止的路径。
- 在方法内部读取且从未存储到实例中的凭据。
- 测试、夹具或示例中的占位值或伪造值。
- 没有生成式序列化,且没有
__dict__或枚举接收端的类型。 - 已对标量键设置允许列表、按凭据名称脱敏,或将对象替换为类型名称的接收端。
- diff 未触及的现有字段和接收端,除非 diff 将它们连接起来。
- 仅涉及风格、命名或缺少测试的观察结果。
修复建议
每项发现都要包含一项具体修复:
- 持有对象:阻止所有生成路径、用可脱敏类型包装该值,或停止将其存储在对象上。
- 原始属性接收端:使用显式允许列表字典进行序列化,而不是序列化实例,或停止将凭据存储在实例上。字段标记或包装无法解决此问题。
- 接收端:对标量键设置允许列表,脱敏匹配凭据名称的键,并对非标量值记录
type(value).__name__,而不是str(value)。 - 回归测试:使用接收端所用的同一序列化器序列化实例(例如
sentry_sdk.serializer.serialize、json.dumps(asdict(obj))、JSON.stringify(obj)、util.inspect(obj)),并断言其中不含凭据字符串。
发现格式
- 标题:注明字段或接收端及泄漏路径,例如“
_shared_secret被包含在RpcClientdataclass 的 repr 中”。 - 描述:用一到两句话指出暴露凭据的生成路径、实例流经的位置以及修复方法。
- 证据:用 2 到 5 条项目符号列出类、装饰器、已检查的生成路径、查找的排除机制,以及追踪到的每个接收端或调用方,包括 diff 之外的部分。
评论
支持 Markdown