From e9ddf89fc20530efcf06da09f509a107d4ec290c Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Thu, 9 Jul 2026 18:13:13 +0800
Subject: [PATCH] fix(secrets): reject inherited exec response ids (#101739)
* fix(secrets): check own exec response ids
* test(secrets): cover inherited exec response errors
---------
Co-authored-by: Peter Steinberger
(cherry picked from commit c765b5de3bdcc9703592b3afa1c88a0e38a1962c)
---
src/secrets/resolve.test.ts | 45 +++++++++++++++++++++++++++++++++++++
src/secrets/resolve.ts | 4 ++--
2 files changed, 47 insertions(+), 2 deletions(-)
diff --git a/src/secrets/resolve.test.ts b/src/secrets/resolve.test.ts
index 902176a34b7b..4ce1411e96f9 100644
--- a/src/secrets/resolve.test.ts
+++ b/src/secrets/resolve.test.ts
@@ -45,6 +45,7 @@ describe("secret ref resolver", () => {
let execPlainScriptPath = "";
let execProtocolV2ScriptPath = "";
let execMissingIdScriptPath = "";
+ let execInheritedErrorScriptPath = "";
let execInvalidJsonScriptPath = "";
let execFastExitScriptPath = "";
@@ -152,6 +153,16 @@ describe("secret ref resolver", () => {
0o700,
);
+ execInheritedErrorScriptPath = path.join(sharedExecDir, "resolver-inherited-error.sh");
+ await writeSecureFile(
+ execInheritedErrorScriptPath,
+ [
+ "#!/bin/sh",
+ 'printf \'{"protocolVersion":1,"values":{"toString":"resolved"},"errors":{}}\'',
+ ].join("\n"),
+ 0o700,
+ );
+
execInvalidJsonScriptPath = path.join(sharedExecDir, "resolver-invalid-json.sh");
await writeSecureFile(
execInvalidJsonScriptPath,
@@ -400,6 +411,40 @@ describe("secret ref resolver", () => {
);
});
+ itPosix("rejects exec refs when missing response id is inherited", async () => {
+ await expect(
+ resolveSecretRefValue(
+ { source: "exec", provider: "execmain", id: "toString" },
+ {
+ config: {
+ secrets: {
+ providers: {
+ execmain: createExecProviderConfig(execMissingIdScriptPath),
+ },
+ },
+ },
+ },
+ ),
+ ).rejects.toThrow('response missing id "toString"');
+ });
+
+ itPosix("ignores inherited exec response errors", async () => {
+ await expect(
+ resolveSecretRefValue(
+ { source: "exec", provider: "execmain", id: "toString" },
+ {
+ config: {
+ secrets: {
+ providers: {
+ execmain: createExecProviderConfig(execInheritedErrorScriptPath),
+ },
+ },
+ },
+ },
+ ),
+ ).resolves.toBe("resolved");
+ });
+
itPosix("rejects exec refs with invalid JSON when jsonOnly is true", async () => {
await expect(resolveExecSecret(execInvalidJsonScriptPath, { jsonOnly: true })).rejects.toThrow(
"returned invalid JSON",
diff --git a/src/secrets/resolve.ts b/src/secrets/resolve.ts
index 8e963656169f..2200ec953eb6 100644
--- a/src/secrets/resolve.ts
+++ b/src/secrets/resolve.ts
@@ -664,7 +664,7 @@ function parseExecValues(params: {
const responseErrors = isRecord(parsed.errors) ? parsed.errors : null;
const out: Record = {};
for (const id of params.ids) {
- if (responseErrors && id in responseErrors) {
+ if (responseErrors && Object.hasOwn(responseErrors, id)) {
const entry = responseErrors[id];
if (isRecord(entry) && typeof entry.message === "string" && entry.message.trim()) {
throw refResolutionError({
@@ -681,7 +681,7 @@ function parseExecValues(params: {
message: `Exec provider "${params.providerName}" failed for id "${id}".`,
});
}
- if (!(id in responseValues)) {
+ if (!Object.hasOwn(responseValues, id)) {
throw refResolutionError({
source: "exec",
provider: params.providerName,