Исходные данные

Итак, предположим у нас есть на фронте React.js, на бэке соответственно DRF. Либо другие аналоги. API бэкенда полностью открыто - как для нашего фронта, так и открыто для postman, scrapy и т.п. Также у нас есть информация, что используя наше же api - конкуренты активно парсят цены, остатки и т.п. Можем ли мы им это запретить? - Не думаю. А вот усложнить им жизнь и развлечься за деньги заказчика сделать это интересным образом - вполне.

Поиск решения

Сразу возникает мысль воткнуть на фронт какой-то токен и дальше со стороны сервера проверять его наличие. Однако разрабы конкурентов вполне способны нажать F12 на сайте, найти этот токен в куках или заголовках запросов и добавить к себе в код парсера - который точно также будет передавать этот же токен.

Вот если бы токен менялся сам по себе постоянно. Что же у нас меняется само по себе?

Так это же время!

Возможно же передавать с фронта время в которое был сделан запрос, а на бэкенде проверять время - когда этот запрос был получен. Кто - то скажет:

- А как же часовые пояса?

А я отвечу:

- Будем использовать timestamp.

Соответственно если в куках/загловках нам приходит с фронта, что запрос был сделан больше чем +-10 минут - значит что-то тут нечисто. Время 10 минут выбрано чисто для примера, думаю вполне можно использовать и 5 минут и 3 минуты и т.д.

Единственная проблема остается: разрабы конкурентов могут модифицировать свой парсер - чтобы он точно также ставил время запроса.

И тут нам приходит на помощь шифрование. Будем использовать симметричное шифрование.

Если мы будем использовать зашифрованный timestamp - который будет генерироваться на фронте реакта, а дешифроваться на бэкенде - то это вполне нас устроит.

Глянем что там у нас есть из существующих алгоритмов. Зашифруем с помощью них пару близких к друг другу timestamp

  • AES

    1640785027 => U2FsdGVkX19OVF5IzcrdnGxJIlenezRUNeqyGuCcHU0=

    1640785140 => U2FsdGVkX19JULnlP8u/ui6LcLYXBW3txJgHNL183DM=

    Как мы видим из-за довольной близости значений есть общая часть и можно понять, что значения слабо меняются от времени да и взглянув на код можно будет понять - что за агоритм используется.

В общем было бы здорово, если бы от изменения считай 1- 2 секунд значение выглядело абсолютно по-другому. Да и учитывая, что код будет по шифрованию на фронте, понятно что минифицированный и обфусцированный. Защита алгоритмом еще никому не помешала.

Что-то получилось?

Пример зашифрованных timestamp

Получилось, достаточно чтобы сбить столку и вывести из строя парсеры конкурентов

1640785595 => 171.148.245.238.228

1640785608 => 216.129.188.192.174

В timestamp быстро меняется самая правая цифра, чем цифра ближе к левой части - тем она реже меняется. Значит мы должны придумать такой алгоритм шифрования - чтобы изменения секунд влияло на весь остальной результат.

Мы можем представить наш timestamp - в виде

16

40

78

55

95

Где каждая ячейка может принимать значения [00; 99]. Понятно, что в первой ячейке уже всегда будут значения >= 16 - но это никак не повлияет на нас.

Когда я взглянул на эти цифры - чем-то они напомнили мне коды символов. И это как раз то что нам нужно. Сгенерируем таблицу шифрования, где ключи в диапазоне [00; 99], а значения уникальные символы. Таблица дешифрования получается - если в таблица шифрования менять местами ключи с значениями.

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()}

Получим таблицу для фронта вида:

let encodeTable = {"00":"ú","01":"ï","02":"€", ... '98': '®', '99': '£'};

И обратную таблицу для дешифровки на бэкенде вида:

decode_table = {'£': '99', ... 'ú': '00', 'û': '78', 'ü': '15','ý': '66'}

Чтобы изменения секунд влияли на весь шифр начнем шифровать именно с них. Развернем нашу строку:

95

55

78

40

16

Будем шифровать ячейки начиная с 95. На это значение влияет только оно само.

Ищем его в шифровочной таблице - получаем '«'

Получаем код символа 171. Это первое число нашего шифра.

Далее шифруем 55. На это значение также влиет пришлое 95. Суммируем эти числа:

55 + 95 = 150. Наша таблица шифрования ограниченная значениями [00;99]. Поэтому возьмем остаток от деления 150 % 100 = 50 в таблице шифрования это '\x94' код это символа 148.

Далее по аналогии шифруем оставшиеся числа. Единственное учитываем момент, что после остатка от деления может быть число < 10 и тогда нужно будет добавать впереди символ нуля, например '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('.');
}

Этот зашифрованный timestamp вида 171.148.245.238.228

Можно добавить в заговолок каждого запроса к примеру. При каждом обновлении страницы данный токен очень сильно отличается и кажется на первый взгляд, что в нем нет логики и он просто случайный.

На бэкенде же дешифрование выполняется в обратном порядке.

  1. Достаем токен из заголовком к примеру

  2. Получаем символ по коду с помощью chr

  3. Если в таблице дешифрования такого символа нет - то это парсер

  4. Достаем символу в таблице число и вычитаем от него предыдущую сумму. Не забываем обработать случай, если сумма больше данного числа

  5. Берем остаток от деления на 100, добавляем символ нуля в начале при необходимости.

  6. Все соединяем, разворачиваем и у нас получился timestamp от фронта

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

Ну а дальше когда знаем парсер это или нет можно как просто возвращать какую-то ерунду, так и добавить к ценам к примеру некий коэффициент.

Главное код на фронте хорошенько запутать. Благо сейчас достаточно сервисов по обфускации. А даже если код прогнать обратно и деобфускацировать - то не зная алгоритма с ходу маловероятно разобраться как генерируются токен.

Быть может кому-то будет полезен мой подход и предложенный вариант реализации.

Комментарии (47)


  1. dopusteam
    02.01.2022 15:09
    +10

    А почему просто авторизацию по какому нибудь jwt не прикрутить?


    1. Kazzman
      02.01.2022 16:10
      +1

      Так пытаются свой фронт от чужого отличать


    1. TerionGVS5 Автор
      02.01.2022 16:14
      +1

      jwt - это классика. И чуть модернизировать парсер, чтобы получить jwt - не вижу в этом больших сложностей. Обхитрить то, что я описал - займет больше времени.


      1. dopusteam
        02.01.2022 17:25
        +2

        Совсем не больше. Вам уже предложили несколько вариантов обхода. В общем случае, если нужно, то обойти такую "защиту" - дело порядка часа.


  1. tuxi
    02.01.2022 15:14
    +7

    Лет 5 уже «развлекаюсь» аналогичным образом, с поправкой на то, что приходится осуществлять защиту всего паблик фронта, а не ендпоинтов API.

    Один из вариантов дальнейшего развития событий: часть скраперов, которых заказчик парсинга ткнет носом в то, что в сданной ими работе на 80..90% фейковые цены или данные, начнут злобно и тупо атаковать другие ендпоинты системы, создавая нагрузку.

    И вот на этом моменте, надо будет иметь под рукой некую готовую к проду систему, которая начнет собирать список их IP из прокси листов, для последующего использования этого списка в целях защиты всего проекта.


    1. TerionGVS5 Автор
      02.01.2022 16:01

      К сожалению все эти прокси IP стоят дешево и их хоть сотнями можно покупать. =(


      1. Bonio
        02.01.2022 16:11

        Листы проксей можно найти в открытых источниках. Не 100%, но большую часть вредоносных запросов можно с их помощью перекрыть.


      1. tuxi
        02.01.2022 16:20

        Да пусть покупают. Тут смысл в том, что если в дальнейшем, например в течении последующих 2..3..4 недель приходят запросы, которые по поведенческому параметру похожи на скраппинг, то наличие таких айпи в том списке, увеличивает вес решения о признании запроса «не несущим профитной составляющей для проекта».


  1. kubrack
    02.01.2022 15:19
    +2

    добавить к ценам к примеру некий коеффциент.

    отличный сюрприз для ваших клиентов, оставляющих открытые страницы со сделаным выбором на потом


    1. Bonio
      02.01.2022 15:28
      +1

      Так токен генерируется из javascript в момент запроса. Хоть на неделю пусть оставляют.


      1. kubrack
        02.01.2022 16:03
        +2

        А, ну да, написал не подумавши, сори, спасибо.
        Но все равно это как-то… by obscurity.
        Ведь все равно этот код отдается на клиент, который может, даже не де-обфусцируя его, просто запускать у себя каждый раз для получения этого поля в парсер.


        Может глупость скажу опять как не фронтендер, но почему не


        1. спроектировать фронт так, чтобы он не перерисовывался по каждому чекбоксу, беся клиентов (привет, Розетка), а, например показывал кнопочку "применить" (во многих магазинах так сделано)
        2. каждому запросу давать открытый одноразовый ключ
        3. придерживать каждый последующий ответ на конкретный запрос (отслеживая по п. 2) на 2-4 секунды (для правильного клиента их не нужно будет чаще т к п. 1). И пусть парсят по ~1000 позиций в час.
        4. ну и в bulk запросах ограничить макс. кол-во результатов разумным числом.


        1. PaulIsh
          02.01.2022 16:19

          По п 2. Инициатор запроса фронт, значит чтобы каждому запросу дать свой одноразовый ключ его должен сформировать фронт. Значит можно фронт разобрать и алгоритм достать.

          По п.3 обычно ставят пакеты (rate limit), которые ограничивают количество запросов с ip в единицу времени.


          1. kubrack
            02.01.2022 16:28

            С ІР нельзя, до сих пор есть много мест где сотни людей за одним ІР. А ключ — просто строка одноразовая от бека, там нечего разбирать и доставать. Можно просто куку если GDPR заморочки уже решены для других кук. Именно чтобы трекать клиента, но не по ІР


            1. Bonio
              02.01.2022 16:54

              А смысл такого ограничения? Поменять строку с кукой на что-то рандомное в парсере, что может быть проще?


              1. kubrack
                02.01.2022 16:58

                Смысл в рейт лимите для каждого реального клиента, не по ір


                1. Bonio
                  02.01.2022 17:00
                  +1

                  А как новго реального клиента отличать? Клиент без кук реальный? А с рандомным мусором в куках?


                  1. kubrack
                    02.01.2022 17:44
                    +1

                    Ну например как клиент, не получивший еще куки, не должен на эндпоинты с данными вообще приходить. А раздачу кук рейтлимитить по IP, жертвуя тем, что первый запрос будет иногда чуть тупить для клиентов, которые сидят толпой на одном ІР.


                    1. PaulIsh
                      02.01.2022 18:35

                      Вы ушли в нюансы, а всё равно скатились к IP. Конечно, не обязательно лимитировать совсем все запросы (хотя лучше все, но поставить лимит адекватный), можете лимитировать которые выдают токен, а дальше ограничивать доступ к api по токену (ну или куки в вашем варианте).


      1. Neikist
        02.01.2022 18:36

        А клиент из деревни где edge и запросы секунд по 40-60 выполняются))


  1. dopusteam
    02.01.2022 15:26
    +3

    симмитричное 

    минифицрованный 

    обфускацированный

    повляет

    коеффциент

    развлечся

    заказщика

    Извините, что не ctrl+enter, но почему бы не проверить перед публикацией?


    1. TerionGVS5 Автор
      02.01.2022 16:22

      Спасибо, действительно, что-то упустил.


  1. PaulIsh
    02.01.2022 15:26
    +5

    Всё равно, если алгоритм на фронте, то достанут и разберут. Может есть ещё варианты как усложнить использование api сторонними инструментами? Webassembly не может помочь спрятать алгоритм?


    1. TerionGVS5 Автор
      02.01.2022 16:05
      +3

      Webassembly - не трогал. Думаю им проще будет парсер переписать на selenium - чем разбираться. Ну в любом случае - пока заметят, что их парсер некорректно работает, пока перепишут, особенно - если с selenium раньше не работали. Каких-то проблем даже такая защита способна доставить)


  1. nik_the_spirit
    02.01.2022 15:30
    +9

    Кажется, вы переизобрели CSRF-токен.


    1. AjnaGame
      02.01.2022 15:36
      +1

      Еще и в странной реализации)))


    1. TerionGVS5 Автор
      02.01.2022 16:00

      Как правило csrf используют для запросов - которые что-то изменяют. Втыкать его во все запросы в том числе простые GET - не хотелось.


  1. Kwisatz
    02.01.2022 15:38
    +14

    Эка вам делать то нечего а. Ну скажите, неужели вы правда серьезно? Ну серьезно, просто качаем клиент и вашими же функциями тоже самое и делаем.


    1. dopusteam
      02.01.2022 15:39
      +2


  1. SergeiMinaev
    02.01.2022 15:41
    +5

    Выше уже подметили про CSRF. Ну и это вам только так кажется, что никто не догадается, что вы там задумали. На деле, очень быстро догадаются. Проверено на личном опыте. К тому же, если будут парсить через selenium, то от этого всего не будет толку. Для себя нашёл единственный более-менее работающий вариант: детектить бота по поведению и показывать капчу.


    1. glader
      02.01.2022 17:11
      +1

      1. AcckiyGerman
        02.01.2022 23:49

        Как-то в году еще 2003 пытался написать бота для популярной браузерной игрушки (fight club или чтото такое) и наткнулся на каптчу, и мне точно такая же идея пришла в голову - нанимать живых людей для решения капчи. Правда я был ленивым студентом, и далее идеи ничего делать не стал.


  1. Andrey_Dolg
    02.01.2022 15:42

    Главное случайно не заблокировать фрон для некоторых из клиентов пока играешься. А касательно парсеров, если информация того стоит(обычно нет) разберутся и спарсят, вопрос времени. Если защита будет сложной, то тот кто разберётся просто будет предоставлять к ней доступ за деньги вот и всё.

    Так же вместо использования api могут начать искать уязвимости, что может быть и неплохо если заметить.


  1. AjnaGame
    02.01.2022 16:07
    +2

    Куда лучше уже рейты ставить т ротлинг. А Ваше решение попусту потраченное время ну и немерянно наивности)


    1. Bonio
      02.01.2022 16:10

      Абсолютной защиты не сделать. Троттлинг и рейт-лимиты тоже не будут работать, если парсер будет работать по спискам проксей.
      А вот усложнить жизнь парсерам в какой-то степени можно, почему бы это не сделать?


      1. AjnaGame
        02.01.2022 16:17
        +1

        Верно, нельзя. Но троттлинг эффективнее решения автора. Плюс, списки проксей тоже можно мониторить и блеклистить, но это уже целый отдельный проект)


  1. vlreshet
    02.01.2022 17:07
    +5

    Я что-то упускаю, или почему нельзя сделать запрос на «настоящую страницу», взять с неё эту куку, и потом уже пульнуть по API? Между этими действиями пройдёт минимум времени, бек ничего не заподозрит. Другими словами — «одноразовость» куки ведь не обеспечивается. Таким образом можно удвоить нагрузку себе на сервер, а парсеры при этом надо будет переписать по-минимуму


  1. Akuma
    02.01.2022 17:30
    +9

    Как владелец парсера, скажу, что вы ничего толком не усложнили :)

    Проблем вы доставите только если будете инфо о товаре выводить картинкой, чтобы не было доступа к текстовой информации. И менять расположение элементов на этой картинке с каждым запросом. Распознавать фото, да еще и меняющееся, сложно и затратно.

    А эта "защита" ничего не стоит, поскольку ваш код для создания токена все так же на клиенте. Банально смотрим в дев-панели откуда был вызван запрос, смотрим исходники и просто копируем себе функцию генерации токена, даже разбирать ее не надо.

    Либо запускаем puppet и просто ходим по страницам. Вообще ничего не надо делать.

    Если хотите усложнить жизнь парсерам, лучше ставьте защиту от ботов на уровне всех запросов. Тот же Cloudflare отлично справляется, но есть и платные шикарные варианты.

    P.S. А вот реальным клиентам это все доставит проблем.


    1. mixsture
      02.01.2022 17:51
      +2

      Проблем вы доставите только если будете инфо о товаре выводить картинкой, чтобы не было доступа к текстовой информации.

      Кстати, один из вариантов у нас недавно ЦИК изобрел. Помните уникальные шрифты для каждой ячейки таблицы? :)
      Для человеческого глаза это незаметно, но заставило тессеракт прикручивать для анализа.


    1. Neikist
      02.01.2022 18:39
      +5

      Проблем вы доставите только если будете инфо о товаре выводить картинкой, чтобы не было доступа к текстовой информации.

      Ух как порадуются слепые/слабовидящие пользователи смотрящие сайт через скринридеры)


  1. mixsture
    02.01.2022 17:49
    +1

    Я бы подошел к этой защите немного с другой стороны: писать бота относительно легко, когда легко видно разграничение — пустило/не пустило. Так вот если «не пустило» будет крайне похоже на «пустило», то стоимость поддержки бота вырастет на порядки.
    Например, на «не пустило» выдавать реальные данные, но примешивать 20% неправильных цен, 20% перемешанных описаний к товарам.


    1. VadimChin
      02.01.2022 20:08
      +1

      ага, если гипермаркет или поставщик для гипермаркетов и руки "мешателей" вдруг оказались не очень прямыми.

      если парсинг открытых данных так сильно влияет на бизнес - у меня плохие новости.


      1. tuxi
        02.01.2022 21:01

        Вы даже не представляете себе какой процент этих «парсеров» написаны в стиле «я дятел, и все что вокруг меня — это деревья, которые надо долбить каждые 2 секунды».

        Ни разу не встречали сайты-копии, контент для которых берется прямо вот онлайн с атакуемого сайта? Никакого промежуточного хранения данных, никакого кеширования хотя бы на 10 минут. Вот прям долбят по списку урлов каждый урл 1 раз в 5 секунд.
        Я вот регулярно такое встречаю.


        1. sden77
          02.01.2022 22:08

          от такого хорошо помогает упомянутая выше защита от ботов с помощью CloudFlare и ему подобных. IP датацентров по умолчанию считаются подозрительными и им чаще выдаётся капча или устанавливается очень низкий порог блокировки


          1. tuxi
            02.01.2022 22:52

            Мы придерживаемся парадигмы «не использовать и не показывать никаких капч».


            1. sden77
              02.01.2022 23:12

              В случае с обращениями к API капч и не будет, они просто будут блокироваться. Такую функциональность можно запросто сделать и самому, списки диапазонов адресов датацентров есть в интернете.


              1. tuxi
                03.01.2022 00:22

                Спасибо кэп! Еще есть ip lookup )))


  1. alexander222
    02.01.2022 18:26
    +2

    для защиты от скрапинга сойдёт наверно, но иногда люди начинают изобретать свои велосипеды алгоритмы для более критичных вещей. И если они не профессиональные математики и специалисты по криптографии, то кончается это обычно не очень хорошо.