T09 · Insecure Skill Coding Practices
- Location
scripts/api-client.mjs:508- Finding
State-Changing Confirmation Tokens Can Be Replayed
- Content
View full analysis
Vulnerability Details
File Location:
scripts/api-client.mjs, lines 508–525 and 592–603
Vulnerability Type: Replayable confirmation token
Risk Level: MediumVulnerable Code
js export const createConfirmationToken = (prepared, env = process.env, nowMs = Date.now()) => { const apiKey = getApiKey(env); const expiresAt = Math.floor(nowMs / 1000) + CONFIRMATION_TTL_SECONDS; return `${expiresAt}.${confirmationMac(prepared, apiKey, expiresAt)}`; }; const verifyConfirmationToken = (actual, prepared, apiKey, nowMs) => { const match = /^(\d{10,12})\.([a-f0-9]{64})$/.exec(actual ?? ''); if (!match) return 'confirmation_mismatch'; const expiresAt = Number(match[1]); const now = Math.floor(nowMs / 1000); if (expiresAt < now) return 'confirmation_expired'; if (expiresAt > now + CONFIRMATION_TTL_SECONDS) return 'confirmation_expiry_invalid'; const expected = confirmationMac(prepared, apiKey, expiresAt); return timingSafeEqual(Buffer.from(match[2], 'hex'), Buffer.from(expected, 'hex')) ? null : 'confirmation_mismatch'; };js if (operation.confirmation.mode === 'hmac-sha256') { const token = createConfirmationToken(prepared, options.env ?? process.env, nowMs); preview = buildPreview(prepared, token); if (operation.confirmation.optIn === 'credential' && options.allowCredential !== true) { return { executed: false, reason: 'credential_opt_in_required', requiredFlag: '--allow-credential', preview }; } if (operation.confirmation.optIn === 'destructive' && options.allowDestructive !== true) { return { executed: false, reason: 'destructive_opt_in_required', requiredFlag: '--allow-destructive', preview }; } if (!options.confirm) return { executed: false, reason: 'confirmation_required', preview }; const confirmationFailure = verifyConfirmationToken(options.confirm, prepared, apiKey, nowMs); if (confirm ...[truncated 2630 chars]- Remediation
View remediation
Remediation Suggestions
- Generate a cryptographically random nonce for every preview and include it in the HMAC-protected token.
- Maintain a trusted consumed-token store keyed by the token identifier or nonce.
- Perform an atomic check-and-consume operation immediately before attempting the network request. Reject tokens that have already been consumed.
- Mark the token consumed even if the request times out or produces an ambiguous transport failure; require a new preview and confirmation for another attempt.
- If durable local state is unsuitable, use a server-enforced idempotency key bound to the canonical request. The server must reject duplicate uses rather than merely deduplicating within the client process.
- Preserve the existing expiration, canonical-request binding, domain separation, and timing-safe HMAC comparison.
- Add automated tests proving that:
- first use succeeds;
- second use of the same token is rejected;
- concurrent uses permit at most one request;
- expired tokens are rejected;
- changed request inputs invalidate confirmation;
- ambiguous failures still consume the token.
- Until replay protection is implemented, avoid claiming that tokens are technically single-use and clearly document that single-use behavior relies on caller discipline.
