Protecting REST API from parsing by generated tokens
Initial data
So, let’s say we have React.js on the front, DRF on the back. Or other analogs. The backend API is completely open – both for our front and open for postman, scrapy, etc. We also have information that using our own api, competitors are actively scrambling prices, balances, etc. Can we forbid them to do this? – I do not think. But to complicate their life and have fun for the customer’s money to do it in an interesting way – quite.

Finding a solution
The idea immediately arises to stick some kind of token on the front and then check its presence from the server side. However, the developers of competitors are quite capable of pressing F12 on the site, finding this token in cookies or request headers and adding a parser to their code – which will also transfer the same token.
Now, if the token was constantly changing by itself. What is changing in our country by itself?
So it’s the same time!
It is possible to transmit from the front the time at which the request was made, and on the backend to check the time – when this request was received. Someone will say:
– What about time zones?
And I will answer:
– We will use timestamp.
Accordingly, if in the cookies / headings it comes to us from the front that the request was made for more than + -10 minutes, then something is unclean here. The time 10 minutes was chosen purely for example, I think it is quite possible to use 5 minutes and 3 minutes, etc.
The only problem remains: competitors’ developers can modify their parser so that it sets the request time in the same way.
And here encryption comes to the rescue. We will use symmetric encryption.
If we use an encrypted timestamp – which will be generated at the front of the reaction, and decrypted at the backend – then this will be fine for us.
Let’s see what we have there from the existing algorithms. Let’s encrypt a pair of timestamps close to each other using them
AES
1640785027 => U2FsdGVkX19OVF5IzcrdnGxJIlenezRUNeqyGuCcHU0 =
1640785140 => U2FsdGVkX19JULnlP8u / ui6LcLYXBW3txJgHNL183DM =
As we can see, due to the rather close proximity of the values, there is a common part and it is possible to understand that the values vary slightly from time to time, and by looking at the code it will be possible to understand what kind of algorithm is used.
In general, it would be great if the value looked completely different from the change in 1 – 2 seconds. And considering that the code will be encrypted at the front, it is clear that minifigured and obfuscated… Algorithm protection has not prevented anyone yet.
Did something work out?
Example of encrypted timestamps
It turned out enough to knock down the table and disable the parsers of competitors
1640785595 => 171.148.245.238.228
1640785608 => 216.129.188.192.174
In the timestamp, the rightmost digit changes rapidly, the closer the digit to the left, the less often it changes. So we have to come up with such an encryption algorithm – so that changes in seconds affect the rest of the result.
We can represent our timestamp – in the form
Where each cell can take values [00; 99]… It is clear that in the first cell there will always be values> = 16 – but this does not affect us in any way.
When I looked at these numbers, they somehow reminded me of character codes. And this is exactly what we need. Let’s generate an encryption table where the keys are in the range [00; 99]and the values are unique characters. The decryption table is obtained if the keys with values are swapped in the encryption table.
from random import randint
def generate_decode_table():
decode_table = {}
for i in range(100):
symbol = chr(randint(128, 254))
while symbol in decode_table:
symbol = chr(randint(128, 254))
decode_table[symbol] = str(i) if i > 9 else f'0{i}'
return decode_table
decode_table = generate_decode_table()
encode_table = {v: k for k, v in decode_table.items()}
We get a table for the front of the form:
let encodeTable = {"00":"ú","01":"ï","02":"", ... '98': '®', '99': '£'};
And a reverse table for decryption on the backend of the form:
decode_table = {'£': '99', ... 'ú': '00', 'û': '78', 'ü': '15','ý': '66'}
So that the changes in seconds affect the entire cipher, we will start encrypting it from them. Let’s expand our line:
We will encrypt cells starting from 95
… This value is influenced only by itself.
We are looking for it in the cipher table – we get '«'
We get the symbol code 171. This is the first number of our cipher.
Next, we encrypt 55
… This value is also influenced by the alien 95
… Let’s summarize these numbers:
55 + 95 = 150
… Our encryption table is limited to values [00;99]… Therefore, we take the remainder of the division 150 % 100 = 50
in the encryption table it is 'x94'
the code of this character 148
…
Further, by analogy, we encrypt the remaining numbers. The only thing we take into account is that after the remainder of the division there may be a number <10 and then it will be necessary to add a zero character in front, for example ’09’
function generateEncodedTimestamp() {
let ts = Math.floor(Date.now() / 1000).toString();
let pairsToEncode = [parseInt(ts.slice(8, 10)), parseInt(ts.slice(6, 8)), parseInt(ts.slice(4, 6)), parseInt(ts.slice(2, 4)), parseInt(ts.slice(0, 2))];
let encodedPairs = [];
pairsToEncode.forEach((el, i) => {
let encodedSum = pairsToEncode.slice(0, i).reduce((a, b) => a + b, 0);
let keyForTable = ((el + encodedSum) % 100).toString();
keyForTable = keyForTable.length > 1 ? keyForTable : `0${keyForTable}`;
encodedPairs.push(encodeTable[keyForTable].charCodeAt(0));
});
return encodedPairs.join('.');
}
This encrypted timestamp is 171.148.245.238.228
You can add to the header of each request for example. Each time the page is refreshed, this token is very different and it seems at first glance that it has no logic and is just random.
On the backend, decryption is performed in reverse order.
We get the token from the title for example
We get the symbol by code using
chr
If there is no such symbol in the decryption table, then this is a parser
We take out the number in the table for the symbol and subtract the previous amount from it. Do not forget to handle the case if the amount is greater than this number
We take the remainder of division by 100, add a zero character at the beginning, if necessary.
We connect everything, unfold and we have a timestamp from the front.
POSSIBLE_DIFF_EPOCH = 300
def is_robot():
seconds_from_epoch_server = int(time.time())
# client_secret = self.request.headers.get('X-VERSION')
client_secret="172.154.130.251.208"
if client_secret is None:
return True
symbols_for_codes = [chr(int(el)) for el in client_secret.split('.')]
reverse_result = []
for index, symbol in enumerate(symbols_for_codes):
try:
encoded_pair = decode_table[symbol]
except KeyError:
return True
if index == 0:
reverse_result.append(encoded_pair)
continue
previous_sum = sum([int(el) for el in reverse_result])
int_encoded_pair = int(encoded_pair)
if int_encoded_pair < previous_sum:
sum_before_division = (previous_sum // 100 + 1) * 100 + int_encoded_pair
else:
sum_before_division = int_encoded_pair
current_pair = str((sum_before_division - previous_sum) % 100)
if len(current_pair) < 2:
current_pair = f'0{current_pair}'
reverse_result.append(current_pair)
seconds_from_epoch_client = int(''.join(reversed(reverse_result)))
if abs(seconds_from_epoch_server - seconds_from_epoch_client) > POSSIBLE_DIFF_EPOCH:
return True
return False
Well, then when we know the parser or not, you can either just return some nonsense, or add a certain coefficient to the prices, for example.
The main code on the front should be confusing. Fortunately, there are enough obfuscation services now. And even if the code is run back and deobfuscated, then without knowing the algorithm on the fly, it is unlikely to figure out how the token is generated.
Perhaps my approach and the proposed implementation option will be useful to someone.