Skip to content

feat(testing): widen bundled default-invariants per security review#753

Merged
bokelley merged 1 commit into
mainfrom
bokelley/widen-bundled-invariants
Apr 22, 2026
Merged

feat(testing): widen bundled default-invariants per security review#753
bokelley merged 1 commit into
mainfrom
bokelley/widen-bundled-invariants

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Security-review on adcontextprotocol/adcp#2769 (which drops local assertion modules in favor of the bundled 5.9 defaults) flagged both idempotency.conflict_no_payload_leak and context.no_secret_echo as shipping materially narrower semantics than the local versions they replace. Widens both to restore coverage before #2769 merges.

Blocker per security-review: idempotency.conflict_no_payload_leak flipped from an allowlist to a 5-field denylist in 5.9, turning key-reuse into an undetected read oracle for a seller inlining budget/product_id/account_id at the adcp_error root. Fix is back to allowlist.

governance.denial_blocks_mutation is untouched.

Widened behavior

idempotency.conflict_no_payload_leak

Before: denylist of 5 field names (payload, stored_payload, request_body, original_request, original_response). A seller putting budget: 5000, start_time: '2026-06-01...' on the envelope passed.

After: allowlist of 7 envelope keys (code, message, status, retry_after, correlation_id, request_id, operation_id). Any other top-level property on the adcp_error envelope fails with a sorted list of leaked field names.

context.no_secret_echo

Before: scanned only response.context; looked for verbatim options.auth_token / options.auth / options.secrets[]. Missed credentials echoed into error.message, audit fields, debug blocks.

After:

  • Full-body recursive walk (not just .context)
  • Bearer-token literal regex /\bbearer\s+[A-Za-z0-9._~+/=-]{10,}/i
  • Suspect property name match at any depth: authorization, api_key, apikey, bearer, x-api-key (case-insensitive)
  • Pick up options.test_kit.auth.api_key as a verbatim-secret source alongside options.auth_token / .auth / .secrets[]
  • All caller-supplied secrets gate on an 8-character minimum so placeholders like "sk" don't false-positive on legitimate protocol vocabulary

Housekeeping

Drop the now-unused extractResponseContext / safeStringify helpers.

Test coverage

16 new unit tests in test/lib/storyboard-default-invariants.test.js:

  • Idempotency: silent on non-IDEMPOTENCY_CONFLICT codes; passes on allowlist-only envelope; fails on non-allowlisted fields (with sorted diagnostic); still fails on the vestigial denylist names; deterministic output on multiple leaks.
  • Context: passes clean responses; fails bearer-token literal in any field; fails verbatim auth_token / secrets[] / test_kit.auth.api_key echo in non-.context locations; fails suspect property names at any depth; walks arrays; ignores short (< 8 char) option values; ignores prose like "bearer of bad news".

38/38 tests passing (up from 22). TypeScript clean.

Backward compatibility

Patch-level — assertion ids, registration API, and passing-case behavior unchanged. The strictening on main has been out for <24 hours in 5.9.0 and no adopters would see breakage in practice (they'd see more violations caught).

Related

🤖 Generated with Claude Code

Security review on adcontextprotocol/adcp#2769 flagged both
idempotency.conflict_no_payload_leak and context.no_secret_echo as
shipping materially narrower semantics than the local versions they
replace. Widens both to restore coverage before #2769 merges.

idempotency.conflict_no_payload_leak:
  Before: denylist of 5 field names (payload, stored_payload,
  request_body, original_request, original_response). Trivially
  bypassed by a seller inlining budget / product_id / account_id at
  the adcp_error root, which turns key-reuse into a read oracle.
  After: allowlist of 7 envelope keys (code, message, status,
  retry_after, correlation_id, request_id, operation_id). Any other
  top-level property on the adcp_error envelope fails with a sorted
  list of leaked field names.

context.no_secret_echo:
  Before: scanned only response.context; missed credentials echoed
  into error.message, audit fields, debug blocks.
  After:
    - Full-body recursive walk (not just .context)
    - Bearer-token literal regex /\bbearer\s+[A-Za-z0-9._~+/=-]{10,}/i
    - Suspect property name match at any depth: authorization,
      api_key, apikey, bearer, x-api-key (case-insensitive)
    - Pick up options.test_kit.auth.api_key as a verbatim-secret
      source alongside auth_token / auth / secrets[]
    - Reuses #752's extractAuthSecrets for the structured auth union,
      including $ENV:VAR resolution via pushCredentialValue
    - Centralises the SECRET_MIN_LENGTH (16) guard in addIfSecret so
      every source gets the same floor

governance.denial_blocks_mutation is untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley force-pushed the bokelley/widen-bundled-invariants branch from b32db2f to 0591c9f Compare April 22, 2026 01:33
@bokelley bokelley merged commit afc01f1 into main Apr 22, 2026
12 checks passed
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