[object Object]

← back to Secrets Manager

Sanitize verifier report fields

2363f17529aa26f370ff4f85f97b7e7dad1ce245 · 2026-08-28 22:24:37 -0700 · Steve Abrams

Files touched

Diff

commit 2363f17529aa26f370ff4f85f97b7e7dad1ce245
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 28 22:24:37 2026 -0700

    Sanitize verifier report fields
---
 cli.js                      | 17 ++++++++++++-----
 test/verify-all.test.js     | 22 +++++++++++++++++++++-
 verification/e2e-proof.json |  4 ++--
 3 files changed, 35 insertions(+), 8 deletions(-)

diff --git a/cli.js b/cli.js
index 6445cde..4e1bfd2 100755
--- a/cli.js
+++ b/cli.js
@@ -305,9 +305,12 @@ async function buildVerifyAllReport({ master, routes = ROUTES, verifier = verify
 }
 
 async function boundedVerify(verifier, key, value, timeoutMs = 10000) {
+  const safeTimeoutMs = Number.isSafeInteger(timeoutMs) && timeoutMs >= 1 && timeoutMs <= 60000
+    ? timeoutMs
+    : 10000;
   let timer;
   const timeout = new Promise((resolve) => {
-    timer = setTimeout(() => resolve({ ok: false, reason: 'timeout' }), timeoutMs);
+    timer = setTimeout(() => resolve({ ok: false, reason: 'timeout' }), safeTimeoutMs);
   });
   try {
     const result = await Promise.race([
@@ -317,10 +320,14 @@ async function boundedVerify(verifier, key, value, timeoutMs = 10000) {
     if (!result || typeof result !== 'object' || typeof result.ok !== 'boolean') {
       return { ok: false, reason: 'malformed-result' };
     }
-    if (!result.ok && !result.reason) {
-      return { ok: false, status: result.status, reason: result.error ? 'network-error' : 'provider-rejected' };
-    }
-    return result;
+    const status = Number.isInteger(result.status) && result.status >= 100 && result.status <= 599
+      ? result.status
+      : null;
+    if (result.ok) return { ok: true, status };
+    const reason = result.reason === 'timeout' || result.reason === 'verifier-error'
+      ? result.reason
+      : result.error ? 'network-error' : 'provider-rejected';
+    return { ok: false, status, reason };
   } finally {
     clearTimeout(timer);
   }
diff --git a/test/verify-all.test.js b/test/verify-all.test.js
index 3c121ed..1ff8970 100644
--- a/test/verify-all.test.js
+++ b/test/verify-all.test.js
@@ -5,7 +5,7 @@ const assert = require('node:assert/strict');
 const fs = require('node:fs');
 const os = require('node:os');
 const path = require('node:path');
-const { buildVerifyAllReport, cmdVerifyAll, main } = require('../cli.js');
+const { boundedVerify, buildVerifyAllReport, cmdVerifyAll, main } = require('../cli.js');
 
 const routes = {
   _comment: { text: 'metadata is not a secret' },
@@ -97,3 +97,23 @@ test('blank secrets count as missing and CLI exit code fails only aggregate FAIL
   assert.equal(await run('warn', 'value', {}, async () => ({ ok: true })), 0);
   assert.equal(await run('pass', 'value', { verify: {} }, async () => ({ ok: true, status: 200 })), 0);
 });
+
+test('allowlists verifier output fields and rejects hostile timeout configuration', async () => {
+  const marker = 'token-do-not-leak';
+  for (const result of [
+    { ok: false, reason: marker },
+    { ok: false, status: { credential: marker } },
+    { ok: true, status: { body: marker } },
+    { ok: false, reason: { body: marker } },
+  ]) {
+    const normalized = await boundedVerify(async () => result, 'TEST_KEY', 'secret', 5);
+    assert.doesNotMatch(JSON.stringify(normalized), new RegExp(marker));
+    assert.ok(normalized.status === null || normalized.status === undefined);
+    if (!normalized.ok) assert.match(normalized.reason, /^(provider-rejected|network-error)$/);
+  }
+
+  for (const timeoutMs of [0, -1, NaN, Infinity, 60001, 1.5]) {
+    const normalized = await boundedVerify(async () => ({ ok: true, status: 204 }), 'TEST_KEY', 'secret', timeoutMs);
+    assert.deepEqual(normalized, { ok: true, status: 204 });
+  }
+});
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index a357586..e8394fb 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -9,11 +9,11 @@
     { "verdict": "PASS", "boundary": "classification", "command": "node --test test/verify-all.test.js", "assertions": "PASS/WARN/FAIL aggregation covers verified, rejected, missing, and no-endpoint keys" },
     { "verdict": "PASS", "boundary": "secret redaction", "command": "node --test test/verify-all.test.js", "assertions": "report serialization contains no input secret values or provider bodies" },
     { "verdict": "PASS", "boundary": "report artifact", "command": "node --test test/verify-all.test.js", "assertions": "temporary data/latest.json round-trips to the returned schema" },
-    { "verdict": "PASS", "boundary": "failure isolation", "command": "node --test test/verify-all.test.js", "assertions": "timeout, throw, and malformed verifier results become sanitized FAIL rows and later checks continue" },
+    { "verdict": "PASS", "boundary": "failure isolation", "command": "node --test test/verify-all.test.js", "assertions": "timeout, throw, malformed, and hostile verifier results become allowlisted rows and later checks continue" },
     { "verdict": "PASS", "boundary": "permissions and exit status", "command": "node --test test/verify-all.test.js", "assertions": "report replacement remains 0600; FAIL exits 1 while PASS/WARN exit 0" },
     { "verdict": "PASS", "boundary": "syntax/diff", "command": "node --check cli.js && git diff --check", "assertions": "CLI parses and diff is clean" }
   ],
-  "negative_checks": ["provider rejection", "configured key missing or blank in master", "key lacking a verify endpoint", "provider body containing the secret", "never-settling verifier", "thrown verifier", "undefined verifier result"],
+  "negative_checks": ["provider rejection", "configured key missing or blank in master", "key lacking a verify endpoint", "provider body containing the secret", "never-settling verifier", "thrown verifier", "undefined verifier result", "verifier-controlled reason/status objects", "invalid timeout configuration"],
   "side_effects": "temporary test report only; no provider request, secret write, route/registry change, launchd install, restart, deploy, or send",
   "cleanup": "temporary directory removed by test",
   "verdict": "PASS for the local report pipeline"

← f585023 Harden secret freshness verification failures  ·  back to Secrets Manager  ·  auto-data-snapshot: 2026-08-29T12:47:37 (2 data files) — reg 87ca939 →