The regulatory requirement for crypto KYC

The Securities and Exchange Commission (SEC) of Nigeria, alongside the Central Bank of Nigeria (CBN), mandates strict Tier 3 equivalent verification for digital asset transactions. Under the SEC rules for Virtual Asset Service Providers (VASPs), any platform facilitating fiat-to-cryptocurrency swaps must establish identity validation. This validation requires a Bank Verification Number (BVN) or a National Identification Number (NIN) combined with a biometric liveness check.

While these rules aim to combat money laundering and fraud, they create a high barrier for user acquisition. Fintech startups experience high drop-off rates when users encounter slow biometric checks, especially on unstable mobile connections. Product teams, desperate to improve conversion rates and prevent user abandonment, face intense pressure to make onboarding as smooth as possible.

The integration gap

Nigerian crypto platforms rely on third-party identity verification aggregators such as Dojah and Smile Identity to connect to government databases. These aggregators provide APIs that wrap queries to the National Identity Management Commission (NIMC) for NIN details and the Nigeria Inter-Bank Settlement System (NIBSS) for BVN records.

The integration gap appears when these government databases experience downtime or latency spikes. The NIMC database, in particular, suffers from frequent degraded performance, with monthly downtime averaging 5-10%. During these outages, a KYC API call that usually takes three seconds will hang and eventually return a 504 Gateway Timeout or a service error response. The application backend must handle this exception. When developers fail to construct a resilient error-handling state machine, they write logic that leaves the platform vulnerable.

The exploit: "fail open" fallback logic

To prevent onboarding blockages during NIMC or NIBSS outages, some engineering teams implement a "fail open" fallback. If the identity API returns a timeout or an internal server error, the backend assumes the user is legitimate but the validation service is temporarily down. Instead of blocking the user, the system proceeds to grant a "verified" status.

Attackers exploit this vulnerability by monitoring government database uptime or scripting automated attempts during periods of known NIMC degradation. When they receive a service timeout, they register multiple synthetic accounts using purchased or public BVNs. The system fails open, allowing unverified users to establish active crypto wallets.

Below is an example of vulnerable backend logic written in Node.js:

// Vulnerable onboarding handler
app.post('/api/kyc/verify', async (req, res) => {
  const { userId, bvn, nin } = req.body;

  try {
    const kycResult = await dojahClient.verifyIdentity({ bvn, nin });
    
    if (kycResult.status === 'SUCCESS' && kycResult.livenessPassed) {
      await db.user.update({
        where: { id: userId },
        data: { kycStatus: 'VERIFIED', tier: 3 }
      });
      return res.json({ success: true });
    }
  } catch (error) {
    // Vulnerable fallback: failing open on timeout or API error
    if (error.status === 504 || error.code === 'PROVIDER_TIMEOUT') {
      await db.user.update({
        where: { id: userId },
        data: { 
          kycStatus: 'VERIFIED_FALLBACK', // Marked internally but treated as active
          tier: 3 
        }
      });
      return res.json({ 
        success: true, 
        message: 'Verification service down. Account active under temporary status.' 
      });
    }
  }
  
  return res.status(400).json({ success: false, error: 'Verification failed' });
});

This logic treats a service failure as a pass. An attacker scripting registrations during a NIMC outage can bypass verification completely.

Parameter tampering bypass

Another common integration mistake involves client-server boundary confusion. Developers sometimes delegate the identity validation call to the mobile application client (iOS/Android) instead of performing it server-to-server.

In this pattern, the mobile app calls the third-party KYC SDK (such as Smile Identity's client-side library) directly. The SDK returns a JSON payload containing the verification status to the client. The mobile app then issues a request to the application backend to update the user's status.

POST /api/users/update HTTP/1.1
Host: api.nigeriancrypto.com
Content-Type: application/json
Authorization: Bearer jwt_token_here

{
  "isKycVerified": true,
  "bvn": "22234567890"
}

An attacker intercepts this request using an interception proxy. They register an account, and when the client app triggers the identity verification step, they modify the outgoing request body. By changing "isKycVerified" from false to true, the attacker updates their verification status without running the verification check against the identity provider.

The backend accepts this update because it relies blindly on the client app's representation of the KYC state.

The business impact

Bypassing identity verification exposes crypto on-ramps to severe legal, financial, and operational penalties.

The fix

To secure your crypto onboarding flows, implement the following architectural controls:

1. Server-to-server validation only

Never allow the client application to report or update the KYC verification status. The mobile or web app must act as a display layer. The application backend must communicate directly with the identity provider (Smile Identity, Dojah) and update the database state based on that trusted response.

2. Fail closed on service timeouts

If the identity API fails or times out, the backend must fail closed. Do not grant verified status. Instead, transition the user account to a pending state and prompt them to retry later. Here is the fix ↓

// Resilient retry/fail-closed logic
if (error.code === 'PROVIDER_TIMEOUT') {
  await db.user.update({
    where: { id: userId },
    data: { kycStatus: 'PENDING_RETRY' }
  });
  return res.status(503).json({ 
    success: false, 
    code: 'KYC_TEMPORARILY_UNAVAILABLE',
    error: 'Identity databases are currently slow. We will notify you when you can resume.' 
  });
}

3. Verify cryptographic callback signatures

When using webhooks for asynchronous verification results, verify the signature of the payload. The provider signs the payload using a shared secret. The backend must calculate the HMAC signature and compare it to the signature header before updating the database.

import crypto from 'crypto';

app.post('/api/kyc/webhook', async (req, res) => {
  const signature = req.headers['x-smile-signature'];
  const payload = JSON.stringify(req.body);
  const secret = process.env.SMILE_WEBHOOK_SECRET;

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  if (signature !== expectedSignature) {
    return res.status(401).json({ error: 'Invalid webhook signature' });
  }

  const { userId, result } = req.body;
  if (result.status === 'Verified') {
    await db.user.update({
      where: { id: userId },
      data: { kycStatus: 'VERIFIED', tier: 3 }
    });
  }

  return res.status(200).json({ received: true });
});

4. Enforce strict database deduplication

Make sure that a BVN or NIN cannot be linked to multiple accounts. Store a cryptographic hash (using HMAC with a pepper key) of the BVN/NIN in the database, and enforce a unique constraint.

// Generate unique hash of the BVN
const bvnHash = crypto
  .createHmac('sha256', process.env.KYC_HASH_PEPPER)
  .update(bvn)
  .digest('hex');

// Check database for existing hash
const existingAccount = await db.user.findUnique({
  where: { bvnHash }
});

if (existingAccount) {
  return res.status(400).json({ error: 'Identity document already linked to another account' });
}
Example finding

Crypto onboarding fallback allows client-side verification override

We tested a crypto wallet onboarding flow where the mobile app made the verification call and then called the API endpoint with the JSON payload { verified: true }. By modifying the payload of our unverified registration, we gained full tier-3 account access. Fix priority: critical.

Automated scanners check for API route availability, but they do not trace client-server trust boundaries or check how fallbacks handle timeout states; manual review finds these logical gaps by modeling the actual boundaries and failures.

Get a security review

Frequently asked questions

Is BVN matching enough for crypto verification?

No, liveness validation is required to confirm the person submitting the BVN actually owns it.

What is the average uptime of the NIMC API?

NIMC API availability fluctuates, with monthly downtime averaging 5-10%.

Can we use automated ID document scanning as a fallback?

Only if it is combined with manual validation or a liveness check, never as a silent, auto-approving fallback.

Related reading

Blog: BVN spoofing and liveness bypass in Tier 1 onboarding · Authentication security testing

Services: Penetration testing

Guides: CBN compliance and security guidelines