Protecting text messages
A sign-up that texts a one-time code, a second factor by SMS, a password reset to a phone: each sends a message you pay for to a number someone else typed. Two attacks use that:
- SMS pumping (toll fraud). Someone who owns a block of numbers in a costly destination — or is paid per message by whoever does — drives your form to text codes to them. Every message is revenue to them and a bill to you, and no code is ever entered. The numbers come in blocks, the requests from rotating addresses.
- SMS bombing. Your form used as a cannon against one person's phone: the same number, texted over and over, often from many addresses.
What is being watched
Send the number with the assessment you make
before texting, as phone, in E.164 form (+447700900123):
- Many numbers from one block of a thousand in ten minutes
(
velocity:numbersInRange): five is a reason (40), ten a challenge on its own (60). From one address, its numbers and the block's are counted once, not twice. - Codes never entered. Once a block has five numbers or more in ten minutes
and almost none of them entered their code — fewer than one in five —
velocity:rangeUnverifiedadds 20. This is the sharpest signal there is, and it needs you to report what happened (below). - Many numbers from one address (
velocity:phonesFromAddress): a person asks for a code to their own number, perhaps a second. Three is a reason (30), six a challenge (60); across an IPv6 source's /48 at sixteen times those (velocity:phonesFromNetwork), so a fresh /64 of a free /48 for each number is no way around it, unless the /48's devices make it a crowd's. From one device (velocity:phonesFromDevice), at the same marks: one browser behind a proxy pool is an address per number, and one device for all of them, counted by the session token's visitor. One campaign seen from the address, the device and the block is counted once, at the largest. - One number, again and again (
velocity:phoneFlood): a person asks again once or twice when a code is slow. Five sends in ten minutes is a phone being bombed (30), ten 50; from five addresses or more,velocity:addressesForPhoneadds 25. - A non-geographic number (
phone:nonGeographic, 40): satellite (+881), international networks (+882, +883), Inmarsat (+870), international premium rate (+979) and the other ITU codes that belong to no country. Nobody verifies a sign-up with one. - The client, as for every assessment — and a valid session token excuses none of it (on a crowd's address, a passed challenge gives way on the address's count of numbers, never the device's): pumping is often driven through a real browser.
The response carries phone: { callingCode, nonGeographic, phonesFromAddress, phonesFromNetwork, phonesFromDevice, addressesForPhone, sendsToPhone, numbersInRange, verifiedInRange } — phonesFromDevice 0 without a session token. The number
and its block are digested on arrival, per environment: neither is stored.
A national format (07700 900123) is not guessed at: an assessment weighs it as
phone:unreadable (20) without its counts, and an outcome report refuses it
with 400 InvalidPayload — which country it belongs to is something only your form knew.
Normalize with the library you already validate numbers with (libphonenumber or
its ports).
Report whether the code was entered
const verdict = await antibot.assess({ ip, headers: req.rawHeaders, method: "POST", url, phone })
if (verdict.action !== "allow") return refuse() // do not send
await sms.send(phone, code)
// …later, when the code is entered, or when it expires unused:
await antibot.reportPhoneOutcome({ ip, phone, outcome: entered ? "success" : "failure" })
The same call exists in every server SDK
(report_phone_outcome, ReportPhoneOutcome, reportPhoneOutcome,
ReportPhoneOutcomeAsync). Reporting only the codes that were entered is
enough: a number that never answers needs no report to count as unverified.
Do not send on a challenge. A challenge on an SMS send has no good answer inside the SMS flow; send a browser to the agent first, or offer another factor.
Your own policy
The strongest defence against pumping is not a score: it is texting only the countries you have customers in. The rule editor's Text only the countries you serve template does that — edit the list:
phone.present and phone.callingCode not in ["1", "44"]
Stop SMS pumping and bombing acts on the counts, with a challenge before
the text is sent — the script does not pass it, and the people behind a
carrier's address, each asking a code for their own number, do — and are not
asked again for the address's count, nor at all on a crowd's address
(ipInfo.crowd), where it is everyone's. The device's own numbers hold it:
phone.numbersInRange >= 5 or (not ipInfo.crowd and not session.challenged and phone.phonesFromAddress >= 5) or phone.phonesFromDevice >= 5 or phone.sendsToPhone >= 6
Rules see phone.present, phone.callingCode, phone.nonGeographic,
phone.phonesFromAddress, phone.phonesFromNetwork, phone.phonesFromDevice, phone.addressesForPhone, phone.sendsToPhone,
phone.numbersInRange and phone.verifiedInRange — see
rules for requests without the agent.
callingCode is the country calling code, not a country: 1 is the whole North
American Numbering Plan, 7 Russia and Kazakhstan, 44 the UK and its Crown
Dependencies.
Knowing it is happening
Edge traffic labels the reasons — "Many numbers from one block (SMS pumping)", "Texted numbers that never enter the code", "One phone texted again and again" — and a wave of them opens an attack alert.