../

Слоупок-ньюс: CSRF в Гемини

Оказывается, отцы-основатели думали о проблеме CSRF уязвимостей ещё на заре Гемини.

A vision for Gemini applications (~solderpunk)

There has been a lot of talk on the Gemini mailing list lately about applications, and dealing with the ugly realities of things like spam and CSRF vulnerabilities

А в относительно недавнем руководстве даже рекомендации на этот случай есть:

Gemini Application Developer Guide

5. Security considerations
5.1 Access tokens
* Use a randomly generated value. Some actions may not be linked to any client certificate, for example if your application supports fully anonymous access, so there is no unique hash to compute. In this case your application can generate random values, periodically updating them with new ones, and check if the requested actions have any valid token value. As a downside, links with tokens will eventually expire, depending on how many old values you are comfortable with keeping stored.

Мой вольный перевод:

* Использование случайно-сгенерированного значения. Некоторые действия не могут быть привязаны к клиентскому сертификату, например, если ваше приложение поддерживает анонимый доступ, то вам не из чего расчитать уникальный хэш. В этом случае ваше приложение может генерировать случайные значения, периодически обновлять их, и проверять, если затребованное действие содержит валидный токен. Как недостаток, ссылки с токенами могут протухать, в зависимости от того как долго вы решите их хранить.

В моём случае "анонимных комментариев" решил не делать сохранение рандомно-сгенерированных токенов, а считать их на лету с привязкой к IP-адресу, пересчитывать каждые пару часов, например. Для бесшовной границы смены часов, генерируется два токена, оба валидные. На час назад, и на текущий. Через час текущий токен сдвигается. Получилась примерно такая фижня:

def csrf_token(ip):
    # ...
    # Округляем ткущее время до часа.
    rolling_period = int(datetime.utcnow().timestamp() / 3600)  # 1 hour

    # Делаем токен для прошлого часа
    token = random.Random(ip + salt + str(rolling_period - 1)).randbytes(5)
    csrf_past = base64...(token)...

    # Делаем токен для текущего часа
    token = random.Random(ip + salt + str(rolling_period)).randbytes(5)
    csrf_now = base64...(token)...
    return csrf_past, csrf_now

def main():
    # ...
    csrf_past, csrf = csrf_token(ip)
    is_csrf_valid = False
    if csrf_past in path or csrf in path:
        # Если один из расчётных токенов есть, то всё хорошо.
        # Иначе путь к файлу странички просто не найдётся, 51 not found.
        path = path.replace(csrf_past, '').replace(csrf, '')
        is_csrf_valid = True
    # ...
    if query == 'reply':
        if is_csrf_valid:
            sys.stdout.write(f'10 {MSG_INPUT}\r\n')
        else:
            # А если при запросе ввода коммента токена ещё не было,
            # то подсунуть пользователю его свежий токен в ссылку
            sys.stdout.write(f'30 {csrf}/{path}?reply\r\n')
    # ...
    else:
        if is_csrf_valid:
            write_comment(dir_, query)
            sys.stdout.write(f'30 ../{path}\r\n')
        else:  # just ignore query
            sys.stdout.write(f'30 {path}\r\n')

Отвратительные диаграммки:

            ┌──────┐                                  ┌──────┐
            │client│                                  │server│
            └──┬───┘                                  └──┬───┘
               │                                         │
  ╔══════════╤═╪═════════════════════════════════════════╪═══════════════════════╗
  ║ SUCCESS  │ │                                         │                       ║
  ╟──────────┘ │             {page}.gmi?reply            │                       ║
  ║            │ ────────────────────────────────────────>                       ║
  ║            │                                         │                       ║
  ║            │   30 Redirect {csrf}/{page}.gmi?reply   │                       ║
  ║            │ <────────────────────────────────────────                       ║
  ║            │                                         │                       ║
  ║            │         {csrf}/{page}.gmi?reply         │                       ║
  ║            │ ────────────────────────────────────────>                       ║
  ║            │                                         │                       ║
  ║            │        10 Input {csrf}/{page}.gmi       │                       ║
  ║            │ <────────────────────────────────────────                       ║
  ║            │                                         │                       ║
  ║            │      {csrf}/{page}.gmi?comment-body     │                       ║
  ║            │ ────────────────────────────────────────>                       ║
  ║            │                                         │                       ║
  ║            │                                         │────┐                  ║
  ║            │                                         │    │ Write comment    ║
  ║            │                                         │<───┘                  ║
  ║            │                                         │                       ║
  ║            │          30 Redirect {page}.gmi         │                       ║
  ║            │ <────────────────────────────────────────                       ║
  ╚════════════╪═════════════════════════════════════════╪═══════════════════════╝
               │                                         │
               │                                         │
  ╔══════════╤═╪═════════════════════════════════════════╪═══════════════════════════════════╗
  ║ INVALID  │ │                                         │                                   ║
  ╟──────────┘ │  {invalid-csrf}/{page}.gmi?comment-body │                                   ║
  ║            │ ────────────────────────────────────────>                                   ║
  ║            │                                         │                                   ║
  ║            │                                         │────┐                              ║
  ║            │                                         │    │ calc valid-csrf              ║
  ║            │                                         │<───┘ path                         ║
  ║            │                                         │      .replace(valid-csrf1, '')    ║
  ║            │                                         │      .replace(valid-csrf2, '')    ║
  ║            │                                         │                                   ║
  ║            │                                         │                                   ║
  ║            │  51 Not found {invalid-csrf}/{page}.gmi │                                   ║
  ║            │ <────────────────────────────────────────                                   ║
  ╚════════════╪═════════════════════════════════════════╪═══════════════════════════════════╝
               │                                         │
               │                                         │
  ╔════════════╪═╤═══════════════════════════════════════╪═══════════════════════════════════╗
  ║ TIME-BORDER  │                                       │                                   ║
  ╟──────────────┘                                       .                                   ║
  ║            .                  16:59                  .                                   ║
  ║            .                                         .                                   ║
  ║            .                                         .                                   ║
  ║            .                                         .                                   ║
  ║            │             {page}.gmi?reply            │                                   ║
  ║            │ ────────────────────────────────────────>                                   ║
  ║            │                                         │                                   ║
  ║            │                                         │────┐                              ║
  ║            │                                         │    │ calc csrf15_00, csrf16_00    ║
  ║            │                                         │<───┘                              ║
  ║            │                                         │                                   ║
  ║            │ 30 Redirect {csrf16_00}/{page}.gmi?reply│                                   ║
  ║            │ <────────────────────────────────────────                                   ║
  ║            │                                         │                                   ║
  ║            │       {csrf16_00}/{page}.gmi?reply      │                                   ║
  ║            │ ────────────────────────────────────────>                                   ║
  ║            │                                         │                                   ║
  ║            │                                         │────┐                              ║
  ║            │                                         │    │ calc csrf15_00, csrf16_00    ║
  ║            │                                         │<───┘ OK path                      ║
  ║            │                                         │      .replace(csrf15_00, '')      ║
  ║            │                                         │      .replace(csrf16_00, '')      ║
  ║            │                                         │                                   ║
  ║            │                                         │                                   ║
  ║            │     10 Input {csrf16_00}/{page}.gmi     │                                   ║
  ║            │ <────────────────────────────────────────                                   ║
  ║            │                                         │                                   ║
  ║            .                                         .                                   ║
  ║            .                  17:01                  .                                   ║
  ║            .                                         .                                   ║
  ║            .                                         .                                   ║
  ║            .                                         .                                   ║
  ║            │   {csrf16_00}/{page}.gmi?comment-body   │                                   ║
  ║            │ ────────────────────────────────────────>                                   ║
  ║            │                                         │                                   ║
  ║            │                                         │────┐                              ║
  ║            │                                         │    │ calc csrf16_00, csrf17_00    ║
  ║            │                                         │<───┘ OK path                      ║
  ║            │                                         │      .replace(csrf16_00, '')      ║
  ║            │                                         │      .replace(csrf17_00, '')      ║
  ║            │                                         │      Write comment                ║
  ║            │                                         │                                   ║
  ║            │                                         │                                   ║
  ║            │          30 Redirect {page}.gmi         │                                   ║
  ║            │ <────────────────────────────────────────                                   ║
  ╚════════════╪═════════════════════════════════════════╪═══════════════════════════════════╝
            ┌──┴───┐                                  ┌──┴───┐
            │client│                                  │server│
            └──────┘                                  └──────┘
Created: 2025-03-26
License: CC0

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

--[ 49 ]--[ 2025-03-30 20:58:33 ]--  

Теперь ещё можно следить за свежими комментами, подписавшись на соответствующую страницу.

💬 Оставить комментарий


Source