secret-serialization

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Secret Serialization Review

密钥序列化审查

Find credentials that can leak because a type serializes itself and something upstream serializes whole objects.
A leak usually needs two changes that each look safe alone:
  1. Holder: a credential is stored as a field on a type with generated
    repr
    ,
    str
    ,
    asdict
    ,
    model_dump
    ,
    toJSON
    , or property enumeration.
  2. Sink: logging, tracing, error reporting, caching, or a response serializes whole objects or every kwarg (
    str(value)
    ,
    span.set_data(key, obj)
    ,
    logger.info("%s", obj)
    ,
    JSON.stringify(args)
    ).
The two sides are often written months apart by different authors. Report either side on its own. Always search the existing tree for the other side, because it is frequently already on the default branch and absent from the diff.
查找因类型自身序列化、且上游环节会序列化整个对象而导致泄露的凭证。
一次泄露通常需要两个单独看起来都安全的变更共同作用:
  1. 持有端(Holder):凭证以字段形式存储在某个类型上,而该类型带有自动生成的
    repr
    、
    str
    、
    asdict
    、
    model_dump
    、
    toJSON
    方法,或是支持属性枚举。
  2. 接收端(Sink):日志、追踪、错误报告、缓存或响应会序列化整个对象或所有kwarg(例如
    str(value)
    、
    span.set_data(key, obj)
    、
    logger.info("%s", obj)
    、
    JSON.stringify(args)
    )。
这两部分代码通常由不同开发者在相隔数月的时间里编写。可单独上报其中任意一端的问题。务必在现有代码库中搜索另一端的情况,因为它往往已经存在于默认分支,而不在当前diff中。

Boundary

适用范围

  • This skill covers indirect leaks through generated serialization and wholesale sinks.
  • Direct leaks, such as interpolating a token into a log message, belong to
    security-review
    . Report them here only when they come from a changed wholesale sink.
  • Hardcoded secret literals and committed
    .env
    files are out of scope.
  • 本技能覆盖通过自动生成的序列化和批量接收端导致的间接泄露。
  • 直接泄露(例如将令牌拼接进日志消息)属于
    security-review
    的范畴。仅当这类泄露来自变更后的批量接收端时,才在此处上报。
  • 硬编码的密钥字面量和已提交的
    .env
    文件不在本技能覆盖范围内。

References

参考资料

ReferenceRead When
references/python.md
Reviewing Python dataclasses, attrs, pydantic, NamedTuple, msgspec, logging, Sentry SDK, or OpenTelemetry code
references/javascript-typescript.md
Reviewing JavaScript or TypeScript classes, config objects,
JSON.stringify
,
util.inspect
, pino or winston, or Sentry and OpenTelemetry spans
参考资料阅读时机
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相关代码时

Credential Fields

凭证字段

Treat a field as credential-bearing when its name, type, or source says so:
  • Names containing
    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
    , or
    refresh_token
    .
  • Types such as
    SecretStr
    ,
    SecretBytes
    , or wrappers around them.
  • Values assigned from environment variables, a secrets manager, or a constructor argument named like the above.
当字段的名称、类型或来源符合以下特征时,将其视为承载凭证的字段:
  • 名称包含
    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
    或其包装类型的字段。
  • 字段值来自环境变量、密钥管理器,或是由名称符合上述特征的构造函数参数赋值的字段。

Explicit Exclusion

显式排除规则

Separate two kinds of path:
  • Generated paths are produced by the type itself:
    __repr__
    and
    __str__
    ,
    asdict
    ,
    model_dump
    ,
    toJSON
    , and own-enumerable-property walks (
    JSON.stringify
    ,
    util.inspect
    , spread). They are the holder's responsibility.
  • Raw attribute access reads the stored value directly:
    vars(obj)
    ,
    obj.__dict__
    ,
    pickle
    ,
    json.dumps(obj, default=vars)
    . Every stored attribute is exposed this way, whatever flags the field has. Treat these only as sinks: report them when a sink in the repository applies one to a credential-bearing instance.
A field is fully excluded when every generated path its type has is blocked, whether by one mechanism or several together. A field is partially excluded when at least one generated path still includes it. No field-level mechanism defeats raw attribute access. Only not storing the value does, so fix raw attribute findings at the sink.
MechanismBlocksStill includes the field
dataclasses.field(repr=False)
,
attrs.field(repr=False)
__repr__
,
__str__
asdict
,
astuple
, raw access
Pydantic
Field(repr=False)
__repr__
,
__str__
model_dump
,
model_dump_json
, response serialization, raw access
Pydantic
Field(exclude=True)
model_dump
,
model_dump_json
, response serialization
__repr__
,
__str__
, raw access
Pydantic
Field(repr=False, exclude=True)
Every generated pathRaw access
JavaScript
#private
field
Every generated pathNothing outside the class body
Object.defineProperty(..., { enumerable: false })
Every generated pathExplicit property reads,
Object.getOwnPropertyNames
Hand-written
__repr__
,
__str__
,
toJSON
, or
[util.inspect.custom]
that omits the field
Only the path it overrides (a
__repr__
also serves
str()
when there is no
__str__
)
Every other generated path, raw access
Redacting wrapper:
SecretStr
,
SecretBytes
, a project
Redacted[T]
Every generated path
pickle
and recursive
__dict__
walks such as
default=vars
, which reach the value stored inside the wrapper
Not stored on the object: read inside the method, or held in a closureEverythingNothing
Do not treat underscore naming, TypeScript
private
,
__slots__
, type annotations, comments,
dataclass(init=False)
, or a custom
__init__
that still assigns the field as blocking any path.
需要区分两种序列化路径:
  • 生成式路径:由类型自身产生的序列化方式,包括
    __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__
视为能阻断任何序列化路径。

Investigation Process

调查流程

  1. Read the changed hunk. Find new or modified credential fields on auto-serializing types, and new or modified code that serializes objects or kwargs into logs, spans, error context, caches, queues, or responses.
  2. For each credential field, read the full class, decorators, base classes, and model config. List every generated path and check which ones the field's mechanisms block, using the table above.
  3. Grep for where instances are constructed and passed. Instances handed to decorators,
    **kwargs
    , tool call arguments, task payloads, or middleware reach generic sinks.
  4. Grep the whole repository for sinks that can see those instances, not only the diff. Useful patterns:
    str(value)
    ,
    repr(
    ,
    asdict(
    ,
    model_dump(
    ,
    set_data(
    ,
    set_attribute(
    ,
    set_context(
    ,
    set_extra(
    ,
    JSON.stringify(
    ,
    util.inspect(
    , loggers called with objects, and raw attribute access (
    vars(
    ,
    __dict__
    ,
    pickle.dumps(
    ,
    default=vars
    ).
  5. For each changed sink, check for a key allowlist, redaction keyed on credential names, or a filter that replaces objects with their type name. Then grep for callers that pass credential-bearing objects into it.
  6. Look at tests only to see whether one asserts the credential is absent from the serialized form. A missing test is fix guidance, not a finding.
  7. Confirm the code ships. Skip fixtures, examples, generated code, and test-only helpers.
  1. 阅读变更的代码块。找出自动序列化类型上新增或修改的凭证字段,以及将对象或kwarg序列化后写入日志、span、错误上下文、缓存、队列或响应的新增/修改代码。
  2. 针对每个凭证字段,阅读完整的类定义、装饰器、基类和模型配置。列出所有生成式路径,并对照上表检查字段的防护机制能阻断哪些路径。
  3. 搜索实例的创建和传递位置。被传入装饰器、
    **kwargs
    、工具调用参数、任务payload或中间件的实例,会接触到通用接收端。
  4. 在整个仓库中搜索能接触到这些实例的接收端,不要只看diff。有用的搜索模式包括:
    str(value)
    、
    repr(
    、
    asdict(
    、
    model_dump(
    、
    set_data(
    、
    set_attribute(
    、
    set_context(
    、
    set_extra(
    、
    JSON.stringify(
    、
    util.inspect(
    、传入对象的日志调用,以及原始属性访问(
    vars(
    、
    __dict__
    、
    pickle.dumps(
    、
    default=vars
    )。
  5. 针对每个变更的接收端,检查是否有键名白名单、基于凭证名称的脱敏规则,或是将对象替换为其类型名称的过滤器。然后搜索向其传入承载凭证对象的调用方。
  6. 查看测试仅为确认是否有断言验证凭证不会出现在序列化结果中。缺少测试属于修复建议,而非问题发现。
  7. 确认代码是线上运行的。跳过fixture、示例代码、生成的代码和仅用于测试的辅助工具。

What To Report

需上报的问题类型

CategoryReport When
Unexcluded credential fieldA credential field is added or moved onto a type with generated serialization, and no mechanism blocks any of its generated paths.
Partial exclusionAt least one generated path still includes the field, and a sink in the repository uses that path on instances of the type.
Raw attribute sinkA sink applies
vars
,
__dict__
,
pickle
, or
default=vars
to an instance that stores a credential, even if the field is fully excluded or wrapped.
Credential moved onto a holderA refactor moves a credential from a lazy read or closure onto an object field.
Wholesale serialization sinkNew or changed telemetry, logging, error-context, cache, or response code serializes every kwarg, every attribute, or whole objects without an allowlist or redaction.
Sink loses its filterA change removes or weakens redaction, an allowlist, or object-to-type-name replacement in an existing sink.
Object passed to telemetryA client, config, or settings object is passed to a span, log, or error-context call that stringifies it.
类别上报条件
未排除的凭证字段新增或迁移的凭证字段位于带有自动序列化能力的类型上,且没有任何机制阻断其生成式路径。
部分排除至少还有一条生成式路径会包含该字段,且仓库中的某个接收端会对该类型的实例使用这条路径。
原始属性访问接收端接收端对存储了凭证的实例使用
vars
、
__dict__
、
pickle
或
default=vars
,即便该字段已被完全排除或被包装。
凭证迁移至持有端重构将凭证从懒加载或闭包中迁移到了对象字段上。
批量序列化接收端新增或变更的遥测、日志、错误上下文、缓存或响应代码,在没有白名单或脱敏的情况下序列化所有kwarg、所有属性或整个对象。
接收端丢失过滤器变更移除或削弱了现有接收端的脱敏规则、白名单,或是对象转类型名称的替换逻辑。
对象传入遥测系统客户端、配置或设置对象被传入会将其字符串化的span、日志或错误上下文调用中。

Severity

严重等级

LevelUse For
highThe credential reaches a sink anywhere in the repository: log line, span attribute, error event, cache entry, persisted row, queue payload, or API response. The other side may predate the diff. Also a new wholesale sink whose existing callers pass credential-bearing objects.
mediumAn unexcluded credential field whose instances leave the constructing module, after a repository search found no sink. Also a wholesale sink with no allowlist and no traced credential-bearing caller.
lowAn unexcluded credential field whose instances never leave the constructing scope.
  • Do not downgrade because the sink or the holder is outside the diff. That is the normal shape of this bug.
  • Choose the lower severity when reachability depends on unproven preconditions.
等级适用场景
高凭证会到达仓库中任意位置的接收端:日志行、span属性、错误事件、缓存条目、持久化行、队列payload或API响应。另一端的代码可能早于当前diff存在。此外,若新增的批量接收端已有调用方向其传入承载凭证的对象,也属于高严重等级。
中未被排除的凭证字段的实例会离开创建它的模块,但在仓库搜索后未找到接收端。此外,没有白名单且未追踪到传入承载凭证对象的调用方的批量接收端,也属于中等等级。
低未被排除的凭证字段的实例永远不会离开创建它的作用域。
  • 不要因为接收端或持有端不在当前diff中就降低严重等级。这是这类漏洞的典型形态。
  • 当可达性依赖于未被证实的前置条件时,选择较低的严重等级。

What Not To Report

无需上报的情况

  • Fully excluded fields, unless a raw attribute sink receives the instance.
  • Partial exclusions when no sink in the repository uses the unblocked path.
  • Credentials read inside a method and never stored on the instance.
  • Placeholder or fake values in tests, fixtures, or examples.
  • Types with no generated serialization and no
    __dict__
    or enumeration sink.
  • Sinks that already allowlist scalar keys, redact by credential name, or replace objects with a type name.
  • Pre-existing fields and sinks the diff does not touch, unless the diff connects them.
  • Style, naming, or missing-test-only observations.
  • 已被完全排除的字段,除非有原始属性访问接收端会接收该实例。
  • 部分排除的情况,但仓库中没有接收端使用那条未被阻断的路径。
  • 在方法内部读取、从未存储在实例上的凭证。
  • 测试、fixture或示例中的占位符或假值。
  • 没有自动序列化能力、且没有
    __dict__
    或枚举类接收端的类型。
  • 已经对标量键设置白名单、按凭证名称脱敏,或是将对象替换为类型名称的接收端。
  • diff未触及的原有字段和接收端,除非diff将二者关联了起来。
  • 仅关于代码风格、命名或缺少测试的观察。

Fix Guidance

修复建议

Include one concrete fix per finding:
  • Holder: block every generated path, wrap the value in a redacting type, or stop storing it on the object.
  • Raw attribute sink: serialize an explicit allowlisted dict instead of the instance, or stop storing the credential on it. A field flag or wrapper does not fix it.
  • Sink: allowlist scalar keys, redact keys matching credential names, and record
    type(value).__name__
    instead of
    str(value)
    for non-scalar values.
  • Regression test: serialize an instance with the same serializer the sink uses (for example
    sentry_sdk.serializer.serialize
    ,
    json.dumps(asdict(obj))
    ,
    JSON.stringify(obj)
    ,
    util.inspect(obj)
    ) and assert the credential string is absent.
每个问题需给出一个具体的修复方案:
  • 持有端:阻断所有生成式路径,将值包装在脱敏类型中,或是停止在对象上存储该值。
  • 原始属性访问接收端:序列化显式白名单的字典而非实例,或是停止在对象上存储凭证。字段标记或包装类型无法修复这类问题。
  • 接收端:对标量键设置白名单,对匹配凭证名称的键进行脱敏,对于非标量值,记录
    type(value).__name__
    而非
    str(value)
    。
  • 回归测试:使用与接收端相同的序列化方式处理实例(例如
    sentry_sdk.serializer.serialize
    、
    json.dumps(asdict(obj))
    、
    JSON.stringify(obj)
    、
    util.inspect(obj)
    ),并断言凭证字符串不存在于结果中。

Finding Format

问题上报格式

  • Title: name the field or sink and the leak path, for example "
    _shared_secret
    included in
    RpcClient
    dataclass repr".
  • Description: one or two sentences naming the generated path that exposes the credential, where instances travel, and the fix.
  • Evidence: 2 to 5 bullets naming the class, decorator, generated paths checked, the exclusion looked for, and every sink or caller traced, including those outside the diff.
  • 标题:指明字段或接收端以及泄露路径,例如「
    RpcClient
    dataclass的repr中包含
    _shared_secret
    」。
  • 描述:用一到两句话说明暴露凭证的生成式路径、实例的流转范围以及修复方案。
  • 证据:用2到5个要点列出相关的类、装饰器、已检查的生成式路径、排查过的排除机制,以及所有追踪到的接收端或调用方(包括不在diff中的)。