Skip to content

GET /api/security/address-check fails open: unconfigured deployments return a fabricated risk score of 50 #823

Description

@nanaf6203-bit

GET /api/security/address-check fails open: unconfigured deployments return a fabricated risk score of 50

Labels / Complexity: bug · security · Medium Complexity — Medium

Problem

src/app/api/security/address-check/route.ts proxies to Chainalysis when CHAINALYSIS_API_KEY is set — but when the key is unset, it fails open with a fabricated score:

if (!apiKey) {
  return NextResponse.json({
    address,
    risk_score: 50,
    categories: ['unknown'],
    description: 'Risk check unavailable (service not configured)',
  });
}

Any unconfigured deployment returns risk_score: 50 for every address — a value the UI can display as a real risk signal (the 50 is not "unknown", it is a specific number, and downstream code may threshold on it). The failure mode is "everyone gets a medium-risk score", not "risk checks are unavailable". This is the API-level counterpart of the simulated checks in src/utils/security/ (tracked separately): both present fabricated numbers as real screening.

Root cause

src/app/api/security/address-check/route.ts (the if (!apiKey) branch): returning a concrete score instead of failing closed with an explicit "unavailable" state.

Why this is architecturally hard

  1. The response contract must encode "unavailable". Clients must be able to distinguish "checked and clean", "checked and risky", and "not configured" — the current response collapses the last two into a number. The fix changes the API's response shape and the UI consumers (useSecurity/WalletAddressInput), so the contract decision comes first.
  2. The route is also an unauthenticated quota proxy. Like /api/simulate, it forwards to Chainalysis with no auth/rate limit; the fix should decide the gate at the same time so the endpoint is both honest and protected.

Acceptance criteria

  • When CHAINALYSIS_API_KEY is unset, the route returns an explicit "unavailable/unconfigured" state (e.g. risk_score: null + a status field) instead of the fabricated 50; clients render "not verified" rather than a numeric risk.
  • A test covers both configured and unconfigured paths, asserting the response contract.
  • The UI consumer renders the unavailable state distinctly.
  • npm run typecheck, npm test, and npm run lint pass.

Out of scope

The simulated checks in src/utils/security/ (tracked separately) and Chainalysis account setup.

Getting started

  • src/app/api/security/address-check/route.ts — the fail-open branch
  • src/hooks/useSecurity.ts, src/components/security/WalletAddressInput.tsx — the consumers of the score

Commands: npm run typecheck, npm test, npm run lint.

Good first files to read: src/app/api/security/address-check/route.ts, src/hooks/useSecurity.ts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Medium ComplexitybugSomething isn't working correctlysecuritySecurity issue or hardening opportunity

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions