How USSD works at the protocol level

When a user dials *901# (MTN MoMo), *737# (GTBank), or any USSD shortcode, the telco's USSD gateway opens a session and assigns it a session ID. That session ID is a string — typically numeric or alphanumeric — that uniquely identifies the ongoing conversation. The gateway then fires an HTTP POST to the fintech's configured webhook (commonly /ussd/callback or /api/ussd) with a payload that looks roughly like this:

POST /ussd/callback HTTP/1.1
Content-Type: application/x-www-form-urlencoded

sessionId=AT-2026-XXXXXXXX
&serviceCode=*384*96673#
&phoneNumber=+2348012345678
&networkCode=MTN
&text=

The text field is empty on the first request (the user has just dialed in). The fintech responds with a string prefixed by CON (continue — show the menu and wait for input) or END (terminate the session). Every time the user presses a key and submits, the gateway fires another POST with the same sessionId and an updated text field containing the full input history, delimited by *:

POST /ussd/callback HTTP/1.1
Content-Type: application/x-www-form-urlencoded

sessionId=AT-2026-XXXXXXXX
&serviceCode=*384*96673#
&phoneNumber=+2348012345678
&networkCode=MTN
&text=1*2*5000

Here 1 was the menu choice (e.g., "Send Money"), 2 selected a beneficiary, and 5000 was the entered amount. The fintech maintains conversation state — current step, account context, selected amount, destination — keyed to the sessionId. That state is usually stored in Redis or an in-memory cache with a TTL matching the session timeout.

Session ID predictability on older gateway hardware

Not all USSD infrastructure is equal. MTN Nigeria, Airtel, Glo, and 9mobile operate USSD gateways running varying generations of Huawei and Ericsson USSD stack software. Older Huawei USN9000 and Ericsson XUDT gateway versions — still active on parts of the Nigerian telco network — have documented weaknesses in how they generate session IDs.

In shared USSD aggregator environments (where multiple fintechs use the same gateway through a provider like Africa's Talking or Infobip), session IDs from these older stacks can exhibit patterns:

An attacker with a USSD account on the same aggregator platform can observe their own session IDs across several sessions to establish the pattern, then predict the IDs assigned to other concurrent sessions. This is not a theoretical edge case — it was the exact mechanism behind documented session interception research on shared USSD infrastructure across West Africa between 2021 and 2024.

Session fixation: injecting into another user's flow

Session fixation in USSD differs from its web equivalent but the core failure is the same: the server accepts a request bearing a known session ID without verifying the request came from the entity that created the session.

The attack path works like this:

  1. Attacker dials the same USSD shortcode from their own SIM. Observes their own sessionId.
  2. Attacker predicts or guesses the sessionId assigned to a concurrent victim session.
  3. Attacker crafts a POST directly to the fintech's /ussd/callback endpoint — not through the telco gateway — using the predicted session ID but substituting their own MSISDN or an arbitrary one:
POST /ussd/callback HTTP/1.1
Content-Type: application/x-www-form-urlencoded

sessionId=AT-2026-XXXXXXXY   ← victim's predicted session ID
&serviceCode=*384*96673#
&phoneNumber=+2348099999999  ← attacker's MSISDN
&networkCode=MTN
&text=1*2*50000              ← attacker-injected input

If the fintech's backend does not validate that the incoming MSISDN matches the MSISDN that initiated the session with this ID, it will process this request as a legitimate step in the victim's session. The attacker has just injected a transfer amount of ₦50,000 into a flow the victim initiated.

The victim will see a confirmation screen (step 4: "Confirm transfer of ₦50,000 to Account XXXX — Enter PIN") that the attacker has constructed. If the victim enters their PIN believing they are confirming a ₦500 airtime purchase they started, the transaction completes for the attacker's injected amount.

Timeout exploitation: the dead session window

USSD sessions have configurable inactivity timeouts — typically 60 to 180 seconds, set either at the telco gateway or the aggregator level. On Nigerian networks, congestion is real: a user in Lagos during peak hours can dial in, receive their menu 30–40 seconds later due to SS7 routing delays, read the menu, and timeout before submitting input.

The vulnerability here is not the timeout itself — it is what happens to the session state after the telco gateway closes the session but before the fintech backend destroys the associated state. That gap depends on how the fintech implements timeout notification. Two common failure patterns:

An attacker who has captured a session ID — from network monitoring, from a shared-aggregator environment, or from social engineering the victim — can wait for the gateway timeout to close the legitimate channel, then submit directly to the fintech's callback endpoint while the server-side state is still live.

The PIN extraction scenario

A nuance that makes USSD session attacks more dangerous than they appear: the attacker does not need the victim's PIN to cause financial harm. Here is why.

In a standard USSD money transfer flow, the steps are roughly:

  1. User dials shortcode → Sees main menu → Selects "Transfer Money"
  2. User selects or enters beneficiary account
  3. User enters amount
  4. User sees confirmation summary → Enters PIN to authorize

If an attacker injects into the session at step 2 (after beneficiary selection) and replaces the amount at step 3, the confirmation screen at step 4 shows the attacker-modified values. The victim sees "Confirm transfer of ₦45,000 to 0801XXXXXXX — Enter your PIN." The victim has no way to know this is not what they entered — the USSD display is a plain text string from the fintech, not an immutable hardware screen.

The victim enters their PIN. PIN verification happens server-side against the session state that the attacker has already modified. The transfer of ₦45,000 executes. The attacker receives the funds and never saw the PIN.

This is categorically different from credential theft. The attacker exploited session state corruption — not the PIN itself.

Practical attack surface: aggregators and direct integrations

Africa's Talking, Infobip, and direct telco USSD gateway integrations all operate on the same fundamental model: the gateway POSTs to the fintech's callback URL, and the fintech responds. The gateway trusts the fintech to validate inputs. The fintech is expected to validate that the MSISDN in the callback matches the session owner.

In practice, most Nigerian fintechs do not implement this validation. Their /ussd/callback handler looks like this:

// Common insecure pattern
app.post('/ussd/callback', async (req, res) => {
  const { sessionId, text, phoneNumber } = req.body;

  // Load session state — NO MSISDN VALIDATION
  const session = await redis.get(`ussd:session:${sessionId}`);

  if (!session) {
    return res.send('CON Welcome to MyBank\n1. Send Money\n2. Check Balance');
  }

  const state = JSON.parse(session);
  // Process state.step using incoming 'text'
  // phoneNumber from req.body is logged but never compared to state.msisdn
  const response = await processStep(state, text);
  await redis.set(`ussd:session:${sessionId}`, JSON.stringify(state), 'EX', 180);

  return res.send(response);
});

The phoneNumber is present in the request. It is often logged. It is almost never compared against the MSISDN stored in session state at initiation. This single missing check is the difference between a secure and an exploitable USSD implementation.

Additionally, many fintech USSD callback endpoints are not IP-restricted to the gateway's known egress IP ranges. Any HTTP client with the correct Content-Type can POST to them. Africa's Talking publishes its gateway IP ranges. Infobip does too. If your callback accepts requests from arbitrary IPs, the attack surface extends to anyone on the internet who can guess or observe a session ID.

Business impact

USSD banking is the primary channel for financial inclusion in Nigeria. MTN MoMo alone had over 5.5 million active wallets as of 2024. Airtel Money, Glo Xtra Time loans via USSD, and USSD-based banking for First Bank, GTBank, UBA, and Access Bank collectively serve tens of millions of transactions monthly — many from users who have no smartphone or data access.

A successful session injection attack against a high-volume USSD flow has several impact layers:

The fix

None of these fixes require third-party libraries or infrastructure changes. They are implementation decisions that can ship in a single backend deployment.

1. Bind session ID to the initiating MSISDN

At session creation (the first callback, where text is empty), store the MSISDN alongside the session state. On every subsequent request, compare the incoming MSISDN to the stored one and reject any mismatch:

app.post('/ussd/callback', async (req, res) => {
  const { sessionId, text, phoneNumber } = req.body;

  const isNewSession = !text || text.trim() === '';

  if (isNewSession) {
    // Initialise session, bind MSISDN
    const newState = {
      msisdn: phoneNumber,
      step: 0,
      data: {},
      createdAt: Date.now(),
    };
    await redis.set(
      `ussd:session:${sessionId}`,
      JSON.stringify(newState),
      'EX', 90  // Hard 90-second TTL
    );
    return res.send('CON Welcome to MyBank\n1. Send Money\n2. Check Balance');
  }

  const raw = await redis.get(`ussd:session:${sessionId}`);
  if (!raw) {
    return res.send('END Session expired. Please dial again.');
  }

  const state = JSON.parse(raw);

  // MSISDN binding check — reject session hijack attempts
  if (state.msisdn !== phoneNumber) {
    // Log the anomaly for fraud investigation
    logger.warn({
      event: 'ussd_msisdn_mismatch',
      sessionId,
      expectedMsisdn: state.msisdn,
      receivedMsisdn: phoneNumber,
    });
    await redis.del(`ussd:session:${sessionId}`); // Destroy compromised session
    return res.send('END Security error. Please contact support.');
  }

  const response = await processStep(state, text);
  await redis.set(`ussd:session:${sessionId}`, JSON.stringify(state), 'EX', 90);
  return res.send(response);
});

2. Generate a secondary session token on your backend

Do not rely solely on the telco-generated session ID. At session initiation, generate a cryptographically random secondary token and store it with the session state. Require this token on every step — embed it as a hidden parameter in the USSD menu string (not visible to the user, but passed back in the text field through a specific menu branch):

import { randomBytes } from 'crypto';

// At session creation
const internalToken = randomBytes(16).toString('hex');
const newState = {
  msisdn: phoneNumber,
  internalToken,
  step: 0,
  data: {},
};
// Store token; validate on each subsequent step

This does not work for all USSD flow designs, but for flows where the fintech controls the full menu structure, it provides a second layer that the telco-side session ID alone cannot provide.

3. Strict server-side session expiry and IP allowlisting

4. Anomaly logging and alerting

Instrument every session event with the sessionId and msisdn. Ship these to your SIEM or log aggregator. Alert on:

Example finding

USSD callback endpoint accepts arbitrary MSISDN against active sessions

During a USSD security review for a Nigerian digital bank, we found that the /ussd/callback endpoint accepted any POST carrying a valid-format session ID, regardless of the MSISDN in the request body. By capturing a session ID from our own test device and replaying it with a different MSISDN — simulating another user's handset — we successfully injected a ₦45,000 transfer amount into an active session that a separate test account had initiated. The victim test account was shown a confirmation screen for ₦45,000 to a beneficiary we had specified, with no indication that anything was wrong. The victim account only needed to enter their PIN to authorize a transaction they had not chosen. The endpoint had no IP restriction, no MSISDN binding, and no anomaly logging. Fix priority: critical. The session binding check was deployed within 48 hours; IP allowlisting followed in the next release cycle.

Can your USSD callback endpoint tell the difference between a legitimate telco gateway request and a crafted POST from an arbitrary HTTP client?

Get a security review

Frequently asked questions

Does Africa's Talking provide session binding by MSISDN?

Africa's Talking passes the MSISDN in every callback request. The responsibility to validate that the MSISDN matches the number that initiated the session belongs entirely to the fintech's backend. The gateway itself does not enforce this — it will forward any callback that carries a valid session ID.

Is USSD more or less secure than mobile app banking?

USSD has a fundamentally smaller attack surface — there is no binary to reverse-engineer, no client-side storage to extract, and no network traffic beyond the telco gateway. Its security depends almost entirely on session management. For low-value transactions, properly implemented USSD is adequate. For high-value transactions, a mobile app with biometric authentication is preferable.

Are USSD sessions encrypted?

USSD traffic between a user's handset and the telco's USSD gateway is not end-to-end encrypted. It travels over the SS7 signaling network, which has well-documented interception vulnerabilities. Traffic between the telco gateway and the fintech's callback URL must use HTTPS with strict certificate validation — plain HTTP on that leg is completely unacceptable.

Related reading

Blog: USSD banking security and penetration testing · USSD application security testing methodology

Services: Penetration testing · Mobile money security

Automated scanners have no concept of USSD session state, telco gateway behaviour, or the difference between a CON and END flow. Finding these issues requires a practitioner who has physically tested USSD flows against real or simulated gateway environments — not a tool that fuzzes HTTP endpoints without understanding the protocol underneath.