fix(secrets): treat env refs as audit-safe auth values

Fix secrets audit env-ref classification and document supported auth SecretRef shorthand.\n\nCo-authored-by: XING <wxinxings@gmail.com>
This commit is contained in:
XING
2026-05-17 07:05:10 +08:00
committed by GitHub
parent 3b2cd0dd1a
commit 6b4d371723
6 changed files with 154 additions and 1 deletions

View File

@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
- Channels/stream previews: contain rejected background draft-stream flushes so preview send failures do not surface as fatal unhandled rejections. Fixes #82712. (#82713) Thanks @coygeek.
- Providers/OpenAI Codex: include base `gpt-5.5` and `gpt-5.4` reasoning metadata in the bundled Codex catalog so `/think xhigh` remains available for those models. Fixes #82744.
- Providers/MiniMax: declare CN endpoint auth aliases in the plugin manifest so `minimax-cn` and `minimax-portal-cn` reuse the correct base auth profiles instead of falling back to unrelated models after 401s. Fixes #63823. Thanks @kamusis.
- Secrets/audit: treat `$VAR` auth-profile values as env SecretRefs and stop reporting env-ref credentials as plaintext, including mixed `keyRef` plus env-ref profile states. Fixes #53998. Thanks @schirloc and @artwalker.
- Agents/model fallback: suppress fallback notices when the active OpenAI Codex runtime reports the same canonical OpenAI model.
- Agents/music generation: remove model-controlled request timeouts, default internal provider requests to five minutes, and keep configured timeouts at a 120-second floor.
- Agents/media generation: stop logging delivered failure summaries as missing message-tool delivery when no generated media was expected.

View File

@@ -88,6 +88,13 @@ Use one object shape everywhere:
{ source: "env", provider: "default", id: "OPENAI_API_KEY" }
```
Supported SecretInput fields also accept exact string shorthands:
```json5
"${OPENAI_API_KEY}"
"$OPENAI_API_KEY"
```
Validation:
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`

View File

@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { parseEnvTemplateSecretRef } from "./types.secrets.js";
describe("parseEnvTemplateSecretRef", () => {
it("parses ${VAR} template syntax", () => {
expect(parseEnvTemplateSecretRef("${OPENAI_API_KEY}")).toEqual({
source: "env",
provider: "default",
id: "OPENAI_API_KEY",
});
});
it("parses $VAR shorthand syntax", () => {
expect(parseEnvTemplateSecretRef("$OPENAI_API_KEY")).toEqual({
source: "env",
provider: "default",
id: "OPENAI_API_KEY",
});
});
it("trims whitespace before matching", () => {
expect(parseEnvTemplateSecretRef(" $FOO_BAR ")).toEqual({
source: "env",
provider: "default",
id: "FOO_BAR",
});
});
it("uses the provided provider alias", () => {
expect(parseEnvTemplateSecretRef("$MY_KEY", "custom")).toEqual({
source: "env",
provider: "custom",
id: "MY_KEY",
});
});
it("rejects lowercase shorthand", () => {
expect(parseEnvTemplateSecretRef("$openai_api_key")).toBeNull();
});
it("rejects partial shell-style strings", () => {
expect(parseEnvTemplateSecretRef("prefix-$OPENAI_API_KEY")).toBeNull();
});
});

View File

@@ -21,6 +21,7 @@ export const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
export const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:"; // pragma: allowlist secret
export const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:"; // pragma: allowlist secret
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
const ENV_SECRET_SHORTHAND_RE = /^\$([A-Z][A-Z0-9_]{0,127})$/;
export type SecretInputStringResolutionMode = "strict" | "inspect";
export type SecretInputStringResolution =
| { status: "available"; value: string; ref: null }
@@ -73,7 +74,8 @@ export function parseEnvTemplateSecretRef(
if (typeof value !== "string") {
return null;
}
const match = ENV_SECRET_TEMPLATE_RE.exec(value.trim());
const trimmed = value.trim();
const match = ENV_SECRET_TEMPLATE_RE.exec(trimmed) ?? ENV_SECRET_SHORTHAND_RE.exec(trimmed);
if (!match) {
return null;
}

View File

@@ -582,6 +582,101 @@ describe("secrets audit", () => {
expect(report.filesScanned).toContain(externalModelsPath);
});
it("does not flag $VAR shorthand env refs in auth profiles as plaintext", async () => {
await writeJsonFile(fixture.authStorePath, {
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
key: "$OPENAI_API_KEY", // pragma: allowlist secret
},
},
});
const report = await runSecretsAudit({ env: fixture.env });
expect(
hasFinding(
report,
(entry) => entry.code === "PLAINTEXT_FOUND" && entry.file === fixture.authStorePath,
),
).toBe(false);
});
it("does not flag ${VAR} env refs in auth profiles as plaintext", async () => {
await writeJsonFile(fixture.authStorePath, {
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
key: "${OPENAI_API_KEY}", // pragma: allowlist secret
},
},
});
const report = await runSecretsAudit({ env: fixture.env });
expect(
hasFinding(
report,
(entry) => entry.code === "PLAINTEXT_FOUND" && entry.file === fixture.authStorePath,
),
).toBe(false);
});
it("still flags auth profile plaintext when an explicit ref is also configured", async () => {
await writeJsonFile(fixture.authStorePath, {
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
key: "sk-leftover-plaintext", // pragma: allowlist secret
keyRef: { source: "env", id: "OPENAI_API_KEY" },
},
},
});
const report = await runSecretsAudit({ env: fixture.env });
expect(
hasFinding(
report,
(entry) =>
entry.code === "PLAINTEXT_FOUND" &&
entry.file === fixture.authStorePath &&
entry.jsonPath === "profiles.openai:default.key",
),
).toBe(true);
});
it.each(["$OPENAI_API_KEY", "${OPENAI_API_KEY}"])(
"does not flag %s auth profile env refs when an explicit ref is also configured",
async (value) => {
await writeJsonFile(fixture.authStorePath, {
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
key: value,
keyRef: { source: "env", id: "OPENAI_API_KEY" },
},
},
});
const report = await runSecretsAudit({ env: fixture.env });
expect(
hasFinding(
report,
(entry) =>
entry.code === "PLAINTEXT_FOUND" &&
entry.file === fixture.authStorePath &&
entry.jsonPath === "profiles.openai:default.key",
),
).toBe(false);
},
);
it("does not flag non-sensitive routing headers in openclaw config", async () => {
await writeJsonFile(fixture.configPath, {
models: {

View File

@@ -291,6 +291,7 @@ function collectAuthStoreSecrets(params: {
refValue: entry.refValue,
defaults: params.defaults,
});
const authoredValueRef = coerceSecretRef(entry.value, params.defaults);
if (ref) {
params.collector.refAssignments.push({
file: params.authStorePath,
@@ -301,6 +302,9 @@ function collectAuthStoreSecrets(params: {
});
trackAuthProviderState(params.collector, entry.provider, entry.kind);
}
if (authoredValueRef) {
continue;
}
if (isNonEmptyString(entry.value)) {
addFinding(params.collector, {
code: "PLAINTEXT_FOUND",