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
| Endpoint | HTTP Methods |
|---|---|
/api/sanctions/nightly | POST, GET |
/api/sanctions/sync | POST, 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_KEYis set and thex-api-keyheader does not match exactly, the request is rejected with401 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
- Update to v0.1.36 immediately.
- Verify
SANCTIONS_SYNC_API_KEYis 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. - Rotate
SANCTIONS_SYNC_API_KEYif you suspect the endpoint was accessed without authorisation prior to this fix. - Review your sync logs (
/api/sanctions/nightlyGET 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.ts — organisationId 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