SMS verification code interface anti-malicious attack SMS anti-theft strategy

The following is the user page interaction. Enter your mobile phone number to get the verification code. The user experience is already super simple.

However, simplicity comes at a cost. In terms of security control, programmers have to think about it.

In the fields of system security, information security, and system security defense, text message theft is a commonplace topic. Our company’s system has also experienced at least three fraudulent attacks. It is common to lose 20,000 to 50,000 text messages each time.

In recent years, with the popularity of login methods such as QQ authorized login and WeChat authorized login, SMS fraud seems to have decreased. However, Internet companies are always accustomed to leaving users’ mobile phone numbers. After all, doing so is very conducive to traffic acquisition.

To log in with SMS verification code, the usual method is graphic verification code. To implement it simply, when the mobile phone number entered by the user changes, the page asynchronously requests the server to generate a graphic verification code interface, the server returns the image file stream, and the page generates a verification code image. The user enters the verification code and then requests the server to obtain the verification code interface. The server will verify whether the verification code entered by the user is correct, and only then will the SMS verification code be sent.

Because graphic CAPTCHAs are transmitted via file streaming, they are difficult to crack. Of course, there are tools for identifying images, but in any case, it is still difficult. What if you don’t recognize the picture? Randomly generate a 4-digit verification code and use credential stuffing as a prank? Obviously, the chance of hitting is also very small. In other words, it is more difficult to conduct malicious attacks using graphical verification codes. When we look at 12306 or other Internet websites, we are constantly asked to select specific graphics, slide puzzles, or select specific text in sequence. This kind of security is quite high.

It is said that Ali’s tricks are even more amazing! It can record the trajectory of the mouse on the page, and then identify that it is operated by a human instead of a machine simulation.

The so-called security, to put it bluntly, is to guard against gentlemen and not against villains. The higher the Tao, the higher the devil. We can only make it safer and minimize the waste of SMS resources caused by malicious attacks. There is no way to be 100% safest.

Back to business!

Our requirement is a passenger registration/login page. Passengers enter their mobile phone number and click to get the verification code. The system will determine if it is a new user or the user status is normal, and will send an SMS verification code. Considering a better user experience, no graphical verification code is added.

This simple operation will be troublesome if it is used by abnormal users. So, how to avoid text message fraud to the greatest extent?

Let’s first analyze the abnormal scenario:

┣ The SMS interface was leaked. In daily office work, everyone neglects to transmit information, resulting in interface leakage.

┣ The interface was intercepted on the network.

┣ The SMS service provider is at fault. This situation cannot be ruled out~

┣ “Inner ghost”, the world is dangerous~

In the above situation, if the SMS interface is running naked, it will be treated as a guinea pig and pranked by others.

Naked SMS verification code interface:

GET /api/sendSmsCode?phone=*** HTTP/1.1
Host a.b.com

Just spell a URL similar to a.b.com/api/sendSmsCode?phone=18812345678 to trigger a text message. Isn’t it exciting to spoof this kind of guinea pig interface?

Next, we will perform security control on this interface.

[First] Necessary parameter verification is indispensable

0.1 Mobile phone number validity verification. ?Cannot be empty? 11 digits? Starts with 1? Verify the first two or three number segments, such as 13, 15, 18, 131/2, 152, 183/6/8/9… (optional , if you are not careful, you may filter out normal numbers). ?Filter special numbers, such as 88888888, 11111111, 22222222, 12345678, 38383838…

[Secondly] Let’s analyze normal browser requests:

▼Request Headers:
POST /api/passenger/sendSms HTTP/1.1
Host: che.shenbianhui.cn
Connection: keep-alive
Content-Length: 43
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Type: application/json;charset=UTF-8
Origin: http://che.shenbianhui.cn
Referer: http://che.shenbianhui.cn/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: pgv_pvi=2428115968; UM_distinctid=170257b17e51b3-01ea5235ff274e-b383f66-e1000-170257b17e6177; Hm_lvt_cb56ec9ce26d8a82ead7aa15af69e6e0=15 81176790,1581304635,1581499290;
▼Request Payload:
{"phone":"17813270522","userType":"driver"}

1.1. Method using POST request

1.2. Determine the request header parameters. The server only receives normal browser requests.

1.2.1 Verify User-Agent. Use User-Agent to prevent 403 Forbidden and security interception when HttpClient sends http requests

1.2.2 Verify Reffer

1.2.3 Add additional parameters in Header. There are strategies for keys or tickets later. These parameters can be appended to the request header.

As for point-to-point attacks, you can also forge the values of User-Agent and Reffer to disguise it as a normal browser request. So, this is not enough. Read on.

[Again] Request limit

The distributed system can directly use the incby of redis to implement counting.

redisUtil.set(key:CommonConstant.MSG_TIMES.concat(today), value:0, seconds:60*5);

redisUtil.incr(key:CommonConstant.MSG_TIMES.concat(today), delta:1L);

2.1 Increase the IP number limit. For B/S type external websites, we cannot perform IP whitelist control. However, for the same IP, within a specified time period, the number of requests must be capped. For example, no more than 50 times in 5 minutes. This will be assessed based on business circumstances.

Malicious requests sometimes use proxy IPs. Of course, there is a cost to using proxy IPs.

2.2 “One size fits all” and “current limiting” Within the specified time period, the total number of requests cannot exceed the threshold. For example, the total number of requests cannot exceed 1,000 times in 5 minutes. This will be assessed based on business circumstances.

It should be noted that the time period for this strategy of limiting the number of requests must be “reasonable”, otherwise it may be in vain. Take the above IP limit as an example. If it is set so that the same IP does not exceed 500 times in a day, it is probably useless. If someone wants to attack you, it will definitely not be like a steady flow of water, but a sudden attack. It may be a sudden attack at 0 o’clock or a sudden attack at 12 o’clock. 2.3 The same mobile phone number cannot request verification codes repeatedly within a specific period of time (such as 30 seconds). We can often see it on the user page. After clicking to obtain the verification code, there will be a countdown prompt in seconds. It cannot be reinitiated during this time. Naturally, the server also needs to do this verification. (Double verification of front and rear ends)

[Fourth]Complicated interface parameters

3.1 Add a key parameter, just like a common signature in the payment interface.

3.1.1 Rules for generating keys: front-end and back-end conventions. At the same time, try to ensure that the key is different for each request. For example: mobile phone number = 18612345678, then key = MD5 (the first three digits of the mobile phone number are 186 + the last three digits of the mobile phone number are 678 + the current time/minute)

Both the front-end and back-end generate keys in this way. The front-end page generates a “signature” through a js script, and the server “verifies the signature”.

It should be noted that a buffer must be reserved for time verification, and the client time and server time are not exactly the same. This can be done using a loop or recursive algorithm.

3.2 A more reliable solution. From the above plan, you can further expand your imagination. Add an API for generating keys on the server side. Once the user changes the mobile phone number, the API is called to obtain a key, and the key is sent when obtaining the verification code. In this way, the short verification interface only needs to check whether the key is consistent every time.

Our usual solution to ensure idempotence is to generate a ticket. When the user submits data, the server verifies the ticket, persists the data only if the ticket matches, and then deletes the ticket; once the server finds that the ticket does not exist, it is considered Illegal request.

3.3 Wouldn’t it be safer if we could eliminate the need to send a mobile phone number to the interface for short-term verification? I once asked my friends in the group to think about this issue. Okay, let’s announce the answer – the solution in 3.2 above, after using the mobile phone number to generate the key, the server saves the relationship between the mobile phone number and the key, and then directly sends the key through the SMS delivery interface, just like the “mimicry” in the biological world. Blind the enemy and protect yourself.

In addition, the key adding strategy mentioned above can add 2 or 3 keys at the same time, thus giving people a kind of “visual” confusion and greater defense against attacks. We know that many Internet systems use salt encryption to implement user passwords, which also makes use of this idea.

Let’s take a look at the API and it will probably look like this.

POST /api/sendSmsCode HTTP/1.1
Host a.b.com
{"phone":"***","key":"092E080F5845904EBCFF5F242A87F4DD","code":"O7b1"}
Header["ticket"]:171149199774508c7f17787b1711252400

It even transforms beautifully into something like the following.

POST /api/sendSmsCode HTTP/1.1
Host a.b.com
{<!-- -->"key":"092E080F5845904EBCFF5F242A87F4DD","ticket":"171149199774508c7f17787b1711252400"}

Combining the control of the above solutions, we can ensure the security of the interface to a great extent.

As long as your thinking does not slip, there are always more methods than difficulties.

BTW, the 3.1 and 3.2 solutions require front-end and back-end cooperation. When we discussed it with the front-end guy in the project team, he was quite proficient in writing VUE, NODEJS, and JavaScript scripts, but he felt that it was pointless to do so. He opened the browser’s debugging tool and said that others would know what was going on at a glance. This solution can only achieve a 1% improvement at best. My point of view: 1. Not everyone knows the existence of this page; 2. Not everyone can find that js code; 3. Not everyone is familiar with the front end. Because he personally created this page and called the interface himself, he understands it very well. Not everyone understands it as well as he does, including us back-end programmers. Reminds me of a saying: If you have a hammer in your hand, everything you look at will be a nail. One’s thoughts influence actions. Perhaps, some people who are technologically paranoid have a little bit of personality and unruliness.