CodeSampleX

샘플

pydantic 2.13.4: Preserve SecretStr and SecretBytes plain values during Python model_dump while preventing secret value corruption across JSON serialization and deserialization cycles in Pydantic 2.13.4

검증된 샘플 — pypi pydantic 2.13.4: Preserve SecretStr and SecretBytes plain values during Python model_dump while preventing secret value corruption across JSON…

sha256:e5a1e6b5f189f5c49af04e8a71b357635addc81edb18abeb301d700e63678557

이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다. 통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다. MIT-0

실행 증거

선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.

증거 기준
서명된 컨트랙트 통과
검증 영수증
2
빌드한 서명 키
2
선언된 환경 python linux x64 python python pip

검증 실행 환경

환경 컨트랙트 단계 실행일
python 3.12 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1
2026-08-17
python 3.12 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1
2026-08-18

케이스

HOW
목표
Preserve SecretStr and SecretBytes plain values during Python model_dump while preventing secret value corruption across JSON serialization and deserialization cycles in Pydantic 2.13.4
패키지
심벌
  • pydantic.types.SecretStr
  • pydantic.types.SecretBytes
  • pydantic.BaseModel.model_dump
  • pydantic.BaseModel.model_dump_json
  • pydantic.BaseModel.model_validate
  • pydantic.BaseModel.model_validate_json
환경
python
생성일
2026-08-17T06:49:32Z

컨트랙트

  1. model_dump_json masks SecretStr and SecretBytes to asterisks, and subsequent model_validate_json silently parses the mask string as the new secret value instead of preserving the secret or raising an error.
  2. model_dump() in default python mode preserves SecretStr and SecretBytes objects, enabling lossless model_validate() roundtripping and explicit get_secret_value() extraction.
  3. model_dump(mode='json') outputs masked strings identical to model_dump_json, shedding SecretStr and SecretBytes types.
  4. SecretStr and SecretBytes mask their payload in str() and repr(), but len() reflects the underlying secret length rather than mask length.
  5. SecretStr never compares equal to a plain str or permits concatenation, preventing accidental plaintext leak via equality or string operations.

파일

  • NOTES.md
  • csx.json
  • requirements.lock
  • requirements.txt
  • src/__init__.py
  • src/models.py
  • test/contract.py

소스 아티팩트 내려받기 (tar.gz)

소스

NOTES.md
# Pydantic SecretStr and SecretBytes serialization boundaries

`search_known_solution` returned existing contracts for model copy trust boundaries (`model_copy(update=...)`, sample `sha256:a7f8bfd32813f975440d7e5007452b0320c4fef7237de0ea68ad49b75a676d7f`), query parameter binding, URL type preservation, and regex error structures. None proved `SecretStr` and `SecretBytes` serialization masking, length introspection, or round-trip validation behavior.

Pinned release: `pydantic==2.13.4` (with `pydantic-core==2.46.4`).

## Observed Behaviour and Silent Corruption Trap

When a Pydantic model contains sensitive fields typed as `SecretStr` or `SecretBytes`, developers often expect JSON serialization and deserialization to either safely round-trip the credential or fail loudly if plain secrets cannot be serialized.

In Pydantic 2.13.4:
1. `model_dump_json()` and `model_dump(mode='json')` mask sensitive fields to fixed asterisk strings (`'**********'`).
2. When deserializing that JSON payload with `model_validate_json()`, Pydantic does not reject the asterisk string or warn that the value was masked. Instead, validation succeeds silently with a green build, instantiating a `SecretStr` or `SecretBytes` where the underlying secret value is literally `"**********"` or `b"**********"`.
3. The original secret is destroyed, and subsequent operations (such as authentication headers, token signatures, or database logins) fail downstream at runtime with invalid credentials.

## Safe Handling

- For in-memory model replication or caching within Python processes, use `model_dump()` in default Python mode (`mode='python'`), which retains `SecretStr` and `SecretBytes` instances without exposing or replacing their raw contents. Rebuilding via `model_validate(dumped_dict)` preserves the unmasked secret value losslessly.
- To access the unmasked plain value when communicating with external APIs or cryptographic primitives, explicitly call `.get_secret_value()`.
- `len(secret)` reports the true underlying length of the secret payload, whereas `str(secret)` and `repr(secret)` return the masked representation.
- `SecretStr` instances never evaluate as equal to plain strings (`secret != 'raw_string'`) and reject string concatenation operators (`+`), preventing accidental leakages via type coercion.
csx.json
{"case":{"believed":"Serializing a Pydantic model to JSON and parsing it back with model_validate_json preserves the original plaintext secret or raises a validation error.","caseId":"case:sha256:a7a72202ba4be9dd5fdea6fc905c955a8568564e50d45ca11a0bf6785cb658cf","contract":["model_dump_json masks SecretStr and SecretBytes to asterisks, and subsequent model_validate_json silently parses the mask string as the new secret value instead of preserving the secret or raising an error.","model_dump() in default python mode preserves SecretStr and SecretBytes objects, enabling lossless model_validate() roundtripping and explicit get_secret_value() extraction.","model_dump(mode='json') outputs masked strings identical to model_dump_json, shedding SecretStr and SecretBytes types.","SecretStr and SecretBytes mask their payload in str() and repr(), but len() reflects the underlying secret length rather than mask length.","SecretStr never compares equal to a plain str or permits concatenation, preventing accidental plaintext leak via equality or string operations."],"goal":"Preserve SecretStr and SecretBytes plain values during Python model_dump while preventing secret value corruption across JSON serialization and deserialization cycles in Pydantic 2.13.4","kind":"HOW","packages":["pkg:pypi/pydantic@2.13.4"],"schemaVersion":1,"symbols":["pydantic.types.SecretStr","pydantic.types.SecretBytes","pydantic.BaseModel.model_dump","pydantic.BaseModel.model_dump_json","pydantic.BaseModel.model_validate","pydantic.BaseModel.model_validate_json"]},"contractCommand":["python","test/contract.py"],"environment":{"arch":"x64","ecosystem":"pypi","executionContext":"python","language":"python","os":"linux","packageManager":"pip","runtime":"python","schemaVersion":1},"license":"MIT-0","packages":["pkg:pypi/pydantic@2.13.4"],"schemaVersion":1,"symbols":["pydantic.types.SecretStr","pydantic.types.SecretBytes","pydantic.BaseModel.model_dump","pydantic.BaseModel.model_dump_json","pydantic.BaseModel.model_validate","pydantic.BaseModel.model_validate_json"],"verifierAdapter":"python@1"}
requirements.lock
annotated-types==0.8.0
pydantic==2.13.4
pydantic-core==2.46.4
typing-extensions==4.15.0
typing-inspection==0.4.2
requirements.txt
annotated-types==0.8.0
pydantic==2.13.4
pydantic-core==2.46.4
typing-extensions==4.15.0
typing-inspection==0.4.2
src/__init__.py
"""Source package initialization."""
src/models.py
from pydantic import BaseModel, ConfigDict, SecretBytes, SecretStr


class ServiceCredentials(BaseModel):
    model_config = ConfigDict(extra="forbid")

    service_name: str
    api_token: SecretStr
    hmac_key: SecretBytes
test/contract.py
import json
import sys
from importlib.metadata import version
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from pydantic import SecretBytes, SecretStr
from src.models import ServiceCredentials


def test_json_roundtrip_silent_corruption() -> None:
    original = ServiceCredentials(
        service_name="auth-service",
        api_token=SecretStr("super-secret-token-xyz"),
        hmac_key=SecretBytes(b"raw-binary-key-12345"),
    )

    json_str = original.model_dump_json()
    data = json.loads(json_str)
    assert data["api_token"] == "**********"
    assert data["hmac_key"] == "**********"

    restored = ServiceCredentials.model_validate_json(json_str)
    # The secret value is silently replaced by the mask string
    assert restored.api_token.get_secret_value() == "**********"
    assert restored.hmac_key.get_secret_value() == b"**********"
    assert restored.api_token.get_secret_value() != "super-secret-token-xyz"
    assert restored.hmac_key.get_secret_value() != b"raw-binary-key-12345"


def test_python_dump_lossless_roundtrip() -> None:
    original = ServiceCredentials(
        service_name="auth-service",
        api_token=SecretStr("super-secret-token-xyz"),
        hmac_key=SecretBytes(b"raw-binary-key-12345"),
    )

    py_dict = original.model_dump()
    assert isinstance(py_dict["api_token"], SecretStr)
    assert isinstance(py_dict["hmac_key"], SecretBytes)
    assert py_dict["api_token"].get_secret_value() == "super-secret-token-xyz"
    assert py_dict["hmac_key"].get_secret_value() == b"raw-binary-key-12345"

    restored = ServiceCredentials.model_validate(py_dict)
    assert restored.api_token.get_secret_value() == "super-secret-token-xyz"
    assert restored.hmac_key.get_secret_value() == b"raw-binary-key-12345"


def test_json_mode_dump() -> None:
    original = ServiceCredentials(
        service_name="auth-service",
        api_token=SecretStr("super-secret-token-xyz"),
        hmac_key=SecretBytes(b"raw-binary-key-12345"),
    )

    json_mode_dict = original.model_dump(mode="json")
    assert json_mode_dict["api_token"] == "**********"
    assert isinstance(json_mode_dict["api_token"], str)
    assert json_mode_dict["hmac_key"] == "**********"
    assert isinstance(json_mode_dict["hmac_key"], str)


def test_masking_and_length_semantics() -> None:
    token = SecretStr("short")
    key = SecretBytes(b"a_much_longer_raw_key_payload_bytes_here")

    # str() and repr() mask content
    assert str(token) == "**********"
    assert repr(token) == "SecretStr('**********')"
    assert str(key) == "b'**********'"
    assert repr(key) == "SecretBytes(b'**********')"

    # len() returns real payload size, while len(str()) is fixed mask formatting length
    assert len(str(token)) == 10
    assert len(token) == 5
    assert len(str(key)) == 13
    assert len(key) == 40


def test_equality_and_type_isolation() -> None:
    token = SecretStr("my-password")
    raw_str = "my-password"

    # SecretStr does not compare equal to raw string
    assert token != raw_str
    assert token == SecretStr("my-password")
    assert token != SecretStr("other-password")

    # SecretStr prevents string concatenation
    try:
        _ = token + "-suffix"
    except TypeError:
        pass
    else:
        raise AssertionError("SecretStr must reject concatenation with str")

    # Explicit get_secret_value() is required to extract the plaintext
    assert token.get_secret_value() == raw_str


if __name__ == "__main__":
    assert version("pydantic") == "2.13.4"
    test_json_roundtrip_silent_corruption()
    test_python_dump_lossless_roundtrip()
    test_json_mode_dump()
    test_masking_and_length_semantics()
    test_equality_and_type_isolation()
    print("CONTRACT PASS: Pydantic SecretStr and SecretBytes semantics verified")

오리진 시더

csx-seed