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:
- Sequential numeric IDs within a time window (e.g.,
1748200001,1748200002,1748200003) - Timestamp-prefixed IDs where only the last 4–6 digits vary and are drawn from a narrow random range
- IDs that incorporate the MSISDN hash in a predictable position, leaking partial phone number information
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:
- Attacker dials the same USSD shortcode from their own SIM. Observes their own
sessionId. - Attacker predicts or guesses the
sessionIdassigned to a concurrent victim session. -
Attacker crafts a POST directly to the fintech's
/ussd/callbackendpoint — 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:
- No timeout webhook: The telco gateway closes the session silently. The
fintech never receives a notification. The Redis key holding session state survives until
its TTL expires — which may be 300 seconds or more, far longer than the gateway timeout.
During this window, anyone who presents the session ID to
/ussd/callbackcan resume it. - Optimistic timeout handling: The fintech sets its own TTL on session state to match the declared gateway timeout but does not actively invalidate. If the TTL and the gateway timeout drift (which happens when clock sync is imprecise), there is a gap where the gateway considers the session dead but the fintech will still serve it.
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:
- User dials shortcode → Sees main menu → Selects "Transfer Money"
- User selects or enters beneficiary account
- User enters amount
- 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:
- Direct financial loss: Session-injected transfers route funds to attacker- controlled accounts. Per-transaction amounts may be modest, but at scale — targeting the session burst during salary payment days — total losses can reach eight figures in Naira within hours.
- CBN regulatory exposure: The CBN's Revised Framework for Mobile Money Services (2021) and subsequent circulars require licensed mobile money operators to implement end-to-end session integrity. A confirmed session hijacking incident triggers mandatory incident reporting within 24 hours, potential suspension of USSD service, and sanctions ranging from ₦2 million to ₦10 million per incident — with repeat incidents risking license revocation.
- NDPA liability: Session injection that exposes a user's account balance, transaction history, or beneficiary list constitutes unauthorized access to personal financial data under the Nigeria Data Protection Act 2023. The Nigeria Data Protection Commission can issue fines up to 2% of annual gross revenue for a first violation.
- Telco relationship risk: MTN and Airtel have clauses in their USSD aggregator agreements allowing them to suspend shortcode access if a fintech is found to have inadequate session security. Losing USSD access mid-operation is an existential event for fintechs whose primary channel is the feature phone user base.
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
- Set Redis TTL to 90 seconds maximum. Do not allow the TTL to be extended beyond this on each step — use a fixed-expiry approach, not a sliding window.
- On session termination (user gets an
ENDresponse), explicitly callredis.del()— do not wait for TTL expiry. - Restrict your
/ussd/callbackendpoint to accept requests only from the IP ranges published by your gateway provider (Africa's Talking, Infobip, or the direct telco egress IPs). Implement this at the load balancer or WAF level, not application level. - Validate at the confirmation step that the amount and destination account in the incoming request match what is in server-side session state. Never trust the user-facing confirmation values from the request body — read them from state only.
4. Anomaly logging and alerting
Instrument every session event with the sessionId and msisdn. Ship
these to your SIEM or log aggregator. Alert on:
- Any
ussd_msisdn_mismatchevent - Session IDs that receive requests from more than one distinct MSISDN
- Callback requests originating from IPs outside your gateway allowlist
- Sessions that reach the PIN-entry step but had the amount or destination modified after step 2
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 reviewFrequently 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.