Skip to content

security: keep the API key out of Marshal and YAML - #3

Open
simonx1 wants to merge 1 commit into
obie:mainfrom
simonx1:security/redact-key-from-serialization
Open

simonx1 wants to merge 1 commit into
obie:mainfrom
simonx1:security/redact-key-from-serialization

Conversation

@simonx1

@simonx1 simonx1 commented Sep 18, 2026

Copy link
Copy Markdown

Providers::Base#inspect has redacted the API key since the provider classes landed. Serialization does not go through inspect, and nothing covered it.

Verified on Ruby 3.2.11, 3.3.8 and 3.4.8:

p = RubyDecisionModel::Providers::Typesafe.new(api_key: "sk-SUPER-SECRET")

p.inspect
# => #<...Typesafe name=:typesafe base_url="https://api.typesafe.ai" api_key=[REDACTED]>

Marshal.dump(p).include?("sk-SUPER-SECRET")   # => true
YAML.dump(p)
# --- !ruby/object:RubyDecisionModel::Providers::Typesafe
# api_key: sk-SUPER-SECRET
# base_url:

YAML.dump(client) leaks too — it walks to the provider, where Marshal.dump(client) merely gives up on the Proc transport.

That is a plaintext key in a cache entry, a background-job argument, a config dump or a crash report, produced by an object that looks redacted when you print it.

The fix

Don't serialize the key. marshal_dump/encode_with write the base URL and nothing else; marshal_load/init_with read the key back from the provider's environment variable, which is where a long-lived process should be getting it anyway.

A provider restored without that variable set has no key, and building a Client from it raises ConfigurationError — a loud failure rather than a Bearer header with nothing after it. api_key stays readable in process; the leak was never deliberate access.

Tests

test/redaction_test.rb, 11 cases: both serializers for every registered provider, the client-level YAML walk, base URL survival, environment round-trip, and the no-key-in-env failure. Suite green on 3.2.11 / 3.3.8 / 3.4.8.


One of a series from a security and API-coverage audit. Branches are independent, each off main, so they can be taken in any order or individually.

🤖 Generated with Claude Code

Providers::Base#inspect has redacted the key since the provider classes
landed, which covers logs and error output. It does not cover
serialization, because neither serializer asks inspect anything.

Verified on 3.3.8 and 3.4.8: Marshal.dump(provider) and
YAML.dump(provider) both contained the key verbatim, and so did
YAML.dump(client) -- the client holds the provider, and YAML walks to it
where Marshal gives up on the Procs. That is a plaintext key in a cache
entry, a Sidekiq argument, a config dump, or a crash report, from an
object that looks redacted when you print it.

Do not serialize it. marshal_dump/encode_with write the base URL and
nothing else; marshal_load/init_with read the key back from the
provider's environment variable, which is where a long-lived process
should be getting it. A provider restored without that variable set has
no key, and building a Client from it raises ConfigurationError -- a
loud failure rather than a Bearer header with nothing after it.

api_key stays readable in process. The leak was never deliberate access.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant