fix(models): avoid non-env SecretRef plaintext persistence#34362
fix(models): avoid non-env SecretRef plaintext persistence#34362ADKcodeXD wants to merge 2 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes a security issue where plaintext key material from non-env The root cause was evaluation order: the old code checked the raw Key changes:
Confidence Score: 4/5
Last reviewed commit: 4610b17 |
| it("does not copy plaintext api keys from non-env SecretRefs into providers", async () => { | ||
| const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-")); | ||
| try { | ||
| await fs.writeFile( | ||
| path.join(agentDir, "auth-profiles.json"), | ||
| JSON.stringify({ | ||
| version: 1, | ||
| profiles: { | ||
| "openai:default": { | ||
| id: "openai:default", | ||
| provider: "openai", | ||
| type: "api_key", | ||
| key: "sk-live-from-exec", | ||
| keyRef: { source: "exec", provider: "default", id: "keychain:openai" }, | ||
| }, | ||
| }, | ||
| providerOrder: { openai: ["openai:default"] }, | ||
| }), | ||
| ); | ||
| const providers: NonNullable<NonNullable<OpenClawConfig["models"]>["providers"]> = { | ||
| openai: { | ||
| baseUrl: "https://api.openai.com/v1", | ||
| api: "openai-completions", | ||
| models: [{ id: "gpt-4.1-mini", name: "GPT", input: ["text"], reasoning: false }], | ||
| }, | ||
| }; | ||
|
|
||
| const normalized = normalizeProviders({ providers, agentDir }); | ||
| expect(normalized?.openai?.apiKey).toBeUndefined(); | ||
| } finally { | ||
| await fs.rm(agentDir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
|
|
||
| it("uses env var name when auth profile uses env SecretRef", async () => { | ||
| const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-")); | ||
| try { | ||
| await fs.writeFile( | ||
| path.join(agentDir, "auth-profiles.json"), | ||
| JSON.stringify({ | ||
| version: 1, | ||
| profiles: { | ||
| "openai:default": { | ||
| id: "openai:default", | ||
| provider: "openai", | ||
| type: "api_key", | ||
| keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, | ||
| }, | ||
| }, | ||
| providerOrder: { openai: ["openai:default"] }, | ||
| }), | ||
| ); | ||
| const providers: NonNullable<NonNullable<OpenClawConfig["models"]>["providers"]> = { | ||
| openai: { | ||
| baseUrl: "https://api.openai.com/v1", | ||
| api: "openai-completions", | ||
| models: [{ id: "gpt-4.1-mini", name: "GPT", input: ["text"], reasoning: false }], | ||
| }, | ||
| }; | ||
|
|
||
| const normalized = normalizeProviders({ providers, agentDir }); | ||
| expect(normalized?.openai?.apiKey).toBe("OPENAI_API_KEY"); | ||
| } finally { | ||
| await fs.rm(agentDir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The fix applies identical logic to both the api_key branch (checking keyRef/key) and the token branch (checking tokenRef/token) in resolveApiKeyFromProfiles. However, the test cases only exercise the api_key path. For a security-relevant change like this, adding parallel test cases for a token-type credential with a non-env tokenRef alongside a populated token field would confirm the token branch is equally protected and lock the behavior against future regressions.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/models-config.providers.normalize-keys.test.ts
Line: 77-143
Comment:
The fix applies identical logic to both the `api_key` branch (checking `keyRef`/`key`) and the `token` branch (checking `tokenRef`/`token`) in `resolveApiKeyFromProfiles`. However, the test cases only exercise the `api_key` path. For a security-relevant change like this, adding parallel test cases for a `token`-type credential with a non-env `tokenRef` alongside a populated `token` field would confirm the `token` branch is equally protected and lock the behavior against future regressions.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b64d2b023e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const keyRef = coerceSecretRef(cred.keyRef); | ||
| if (keyRef) { | ||
| if (keyRef.source === "env" && keyRef.id.trim()) { | ||
| return keyRef.id.trim(); | ||
| } |
There was a problem hiding this comment.
Use resolved profile key before env SecretRef id
When secrets runtime is active, profiles can legitimately carry both plaintext and ref fields (src/agents/auth-profiles.runtime-snapshot-save.test.ts:45-50 shows key + keyRef together). This change now returns keyRef.id for env refs before checking cred.key, so callers lose the already-resolved secret. In src/agents/models-config.providers.ts:1085-1092, vLLM discovery passes vllmProfileKey directly as a Bearer token; with env refs that use custom var names, this sends the literal env var name instead of the resolved key and breaks model discovery/auth even though the runtime snapshot has the valid key.
Useful? React with 👍 / 👎.
|
Thanks for the work on this PR and for contributing to this fix path. Closing this as superseded by #38955 (merged), which integrated the relevant fixes from this thread and completed the broader hardening scope for the secrets/models persistence cluster. The merged work now covers source-snapshot persistence, marker/provenance auth handling, audit/apply expansion (including models.json/header residues), and write-mode hardening/tests/docs in one end-to-end fix. Appreciate the contribution here. If there’s a remaining unique change from this PR that should still land, please call it out in a follow-up issue and reference #38955. |
Summary
This PR prevents plaintext key material from being serialized to disk when normalizing provider configs that use non-env
SecretRefvalues.Problem
For non-env
SecretRefinputs, the normalize-keys path could carry through plaintext fields in serialized output, risking accidental persistence of sensitive material.Fix
SecretRefstrips plaintext key fields before serialization.SecretRefunchanged.Verification
node node_modules/vitest/vitest.mjs run src/agents/models-config.providers.normalize-keys.test.tsCloses #34335.