All Docs
FeaturesCalmony Sanctions MonitorUpdated March 11, 2026

Security Advisory: SEC-04 — API Key Bypass on Sanctions Sync Endpoints (Fixed in v0.1.36)

Security Advisory: SEC-04 — API Key Bypass on Sanctions Sync Endpoints

Fixed in: v0.1.36
Severity: High (OWASP)
Type: Authentication bypass
Affected versions: All versions prior to v0.1.36


Overview

Versions of Sanctions Monitor prior to v0.1.36 contained an authentication bypass in the two sanctions data sync endpoints. A flawed conditional fallback allowed any request carrying an Authorization: Bearer header to skip the x-api-key check entirely. This was classified as SEC-04 and rated High severity under OWASP.

The vulnerability is fully resolved as of v0.1.36. All deployments should update immediately.


Affected Endpoints

EndpointHTTP Methods
/api/sanctions/nightlyPOST, GET
/api/sanctions/syncPOST, GET

Vulnerability Detail

The flawed check

Both routes were intended to be protected exclusively by an API key supplied in the x-api-key request header, matched against the SANCTIONS_SYNC_API_KEY environment variable. However, the check included a conditional fallback:

// /api/sanctions/nightly — BEFORE (vulnerable)
if (expectedKey && apiKey !== expectedKey) {
  const authHeader = req.headers.get("authorization");
  if (!authHeader || !authHeader.includes("Bearer")) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  // ↑ If Authorization: Bearer <anything> is present, execution continues.
}

The /api/sanctions/sync route was even more permissive — it fell through to the handler if any Authorization header was present, without requiring a Bearer token at all.

Compounding factor: public middleware routes

Both endpoints were also listed in publicRoutes inside middleware.ts. This meant the NextAuth middleware skipped session validation before requests reached the route handlers, so the only gate was the (broken) API key check.

Impact

Any party with access to the application — including any logged-in user with a valid NextAuth session, or any script that set an Authorization: Bearer header — could trigger a sanctions list sync without possessing SANCTIONS_SYNC_API_KEY. This could result in:

  • Data integrity issues — sanctions entity records overwritten during an active review workflow
  • Audit trail pollution — spurious sync events recorded in the compliance log
  • Excessive re-screening costs — batch rescreening triggered arbitrarily, consuming API quota
  • Compliance audit failures — unexplained sync events in a regulated environment

Fix

Strict API key enforcement (v0.1.36)

The conditional fallback has been removed from all four handlers (POST and GET on both routes). The rule is now unconditional:

If SANCTIONS_SYNC_API_KEY is set and the x-api-key header does not match exactly, the request is rejected with 401 Unauthorized — no fallback, no exceptions.

// AFTER (fixed) — applied to POST and GET on both routes
const apiKey = req.headers.get("x-api-key");
const expectedKey = process.env.SANCTIONS_SYNC_API_KEY;
if (expectedKey && apiKey !== expectedKey) {
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

Middleware hardening

/api/sanctions/sync and /api/sanctions/nightly have been removed from publicRoutes in middleware.ts. A comment now makes the intent explicit: these routes are not public and must not be treated as such.


Required Action for Operators

  1. Update to v0.1.36 immediately.
  2. Verify SANCTIONS_SYNC_API_KEY is set in your deployment environment. If the variable is unset, the key check is skipped entirely — the variable must be configured for the gate to be active.
  3. Rotate SANCTIONS_SYNC_API_KEY if you suspect the endpoint was accessed without authorisation prior to this fix.
  4. Review your sync logs (/api/sanctions/nightly GET endpoint, now also key-gated) for unexpected entries indicating unauthorised trigger activity.

Calling the Sync Endpoints After v0.1.36

All callers (cron jobs, GitHub Actions workflows, external schedulers) must supply the API key via the x-api-key header. The Authorization: Bearer header is no longer accepted as an alternative.

# Correct — trigger nightly sync
curl -X POST https://<your-domain>/api/sanctions/nightly \
  -H "x-api-key: <SANCTIONS_SYNC_API_KEY>"

# Correct — trigger sync for a specific source
curl -X POST "https://<your-domain>/api/sanctions/sync?source=OFAC" \
  -H "x-api-key: <SANCTIONS_SYNC_API_KEY>"

# Correct — check sync status
curl https://<your-domain>/api/sanctions/sync \
  -H "x-api-key: <SANCTIONS_SYNC_API_KEY>"

Requests without a valid x-api-key header will receive:

HTTP/1.1 401 Unauthorized
{ "error": "Unauthorized" }

Related Changes

rbac.tsorganisationId update guard

As part of the same release, the getOrCreateUserProfile function in src/lib/rbac.ts received a tighter guard on organisationId updates. Previously, the absence of an options argument was not reliably distinguished from an explicit organisationId: null, which could trigger spurious database UPDATE statements. The fix uses an explicit hasIncomingOrgId boolean so that a WHERE-matched update only runs when organisationId was intentionally passed by the caller.


Released: v0.1.36 · PR #59 · GitHub Release