Skip to content

fix: fail-closed compliance screening and add server-side enforcement in payout - #27

Open
memosr wants to merge 1 commit into
circlefin:masterfrom
memosr:fix/compliance-fail-closed
Open

fix: fail-closed compliance screening and add server-side enforcement in payout#27
memosr wants to merge 1 commit into
circlefin:masterfrom
memosr:fix/compliance-fail-closed

Conversation

@memosr

@memosr memosr commented Jun 3, 2026

Copy link
Copy Markdown

Problem

Two issues combine into a fail-open compliance vulnerability.

Issue 1 — Compliance screening defaults to PASS on errors

app/api/compliance/screen/route.ts:87-93 returns HTTP 500 with result: "PASS" on any exception (Circle API timeout, DNS failure, rate limit, malformed response):

return NextResponse.json(
  { success: false, result: 'PASS', message: '...Proceeding with caution.' },
  { status: 500 }
);

The frontend callers (components/send-button.tsx, components/transfer-dialog.tsx) call response.json() without checking response.ok, store the body in complianceData, and only block the Send action when complianceData?.result === "FAIL". A 500 body with result: "PASS" is indistinguishable from a clean 200 PASS at the UI layer, so the Send button activates.

Issue 2 — No server-side compliance enforcement in payout

Neither /api/payout/route.ts nor /api/wallet/transfer/route.ts performs a server-side compliance re-check. Screening is advisory client-side only. A direct curl to /api/payout bypasses compliance entirely, regardless of the fail-open bug above.

Exploit path

A sanctioned address submits a transfer. The attacker triggers (or waits for) a transient failure in Circle's screening service. The 500+PASS response flows to the client, the Send button enables, and the transfer executes. No server-side backstop catches it.

Fix

Part 1 — Fail-closed in compliance/screen/route.ts

  } catch (error) {
    console.error('Compliance screening error:', error);
    return NextResponse.json(
      {
        success: false,
-       result: 'PASS',
-       message: 'Compliance screening service is currently unavailable. Proceeding with caution.',
+       result: 'FAIL',
+       message: 'Compliance screening unavailable. Transaction blocked.',
      },
-     { status: 500 }
+     { status: 503 }
    );
  }

503 Service Unavailable is semantically correct here and won't look like a success to any future HTTP-layer guards.

Part 2 — Server-side compliance enforcement in payout/route.ts

Reused the existing screenAddress and mapComplianceResult utilities (the same functions compliance/screen/route.ts uses — no duplication). After input validation and chain resolution, the recipient address is screened on Circle. Any result other than "PASS" (i.e. "FAIL" or "REVIEW") returns 403 before wallets are fetched, balances checked, or any on-chain action taken.

A direct curl to /api/payout now goes through the same compliance gate as the UI.

Impact

  • Security: Closes fail-open vulnerability — compliance failures and errors now block the transaction.
  • Defense in depth: Server-side enforcement means client-side bypass attempts (direct API calls) fail.
  • Risk: Low — legitimate users see no behavior change. Transient compliance API failures will surface as a clear 503 error instead of silently approving the transfer.

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