Статьи

Практическое решение уязвимости BREACH

Две недели назад CERT выпустил рекомендацию по новой уязвимости под названием BREACH . В рекомендациях говорится, что практического решения этой уязвимости нет. Я считаю, что я нашел практическое решение, которое мы, вероятно, реализуем в защите CSRF от Play Frameworks.

Некоторый фон

Прежде всего, что такое уязвимость BREACH? Я рекомендую вам прочитать эту рекомендацию, нет смысла повторять ее здесь, но для тех, кто ленится, вот краткое изложение. Необходимые условия для использования этой уязвимости:

  1. На целевой странице должен использоваться HTTPS, предпочтительно с потоковым шифром (например, RC4), хотя его можно использовать при использовании блочных шифров с дополнением (например, AES)
  2. На целевой странице должно использоваться сжатие уровня HTTP, например, gzip или deflate.
  3. Целевая страница должна давать ответы со статическим секретом. Типичным примером может быть токен CSRF в форме.
  4. Целевая страница также должна отражать параметр запроса в ответе. Также возможно использовать, если в ответе отражены значения тела формы POSTED.
  5. В противном случае ответы должны быть достаточно статичными. Динамические ответы, особенно те, которые изменяют длину ответа, намного дороже в использовании.
  6. Злоумышленник должен иметь возможность прослушивать соединение и, в частности, измерять длину зашифрованных ответов.
  7. Злоумышленник должен иметь возможность заставить браузер жертвы многократно запрашивать целевую веб-страницу.

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

Некоторые обходные пути

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

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

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

Подписанные одноразовые номера

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

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

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

Приложение будет представлять секреты с использованием двух типов токенов, один из которых является «необработанным токеном», который является просто необработанным секретом, а другой – «подписанным токеном». Подписанные токены – это токены, для которых при каждом использовании генерируется одноразовый номер. Этот одноразовый номер объединяется с необработанным токеном, а затем подписывается. Алгоритм для этого в Scala может выглядеть так:

1
2
3
4
5
def createSignedToken(rawToken: String) = {
  val nonce = System.currentTimeMillis
  val joined = rawToken + "-" + nonce
  joined + "-" + hmacSign(joined)
}

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

Каждый раз, когда токен отправляется в ответе, он должен быть вновь сгенерированным подписанным токеном. Хотя можно публиковать необработанный токен в заголовках HTTP-ответа, чтобы избежать путаницы в отношении того, какие входящие токены должны быть подписаны, а какие могут быть необработанными, я рекомендую всегда публиковать и принимать только подписанные токены. При сравнении токенов подпись должна проверяться на каждом токене, и если он проходит, то нужно сравнивать только необработанную часть токенов. Алгоритм извлечения необработанного токена из подписанного токена, созданного с использованием вышеуказанного алгоритма, может выглядеть следующим образом:

1
2
3
4
5
6
7
8
9
def extractRawToken(signedToken: String): Option[String] = {
  val splitted = signedToken.split("-", 3)
  val (rawToken, nonce, signature) = (splitted(0), splitted(1), splitted(2))
  if (thetaNTimeEquals(signature, hmacSign(rawToken + "-" + nonce))) {
    Some(rawToken)
  } else {
    None
  }
}

где thetaNTimeEquals выполняет сравнение строк со временем Θ (n), когда длины строк равны, чтобы предотвратить атаки по времени. Проверка соответствия двух токенов может выглядеть следующим образом:

1
2
3
4
5
6
7
def compareSignedTokens(tokenA: String, tokenB: String) = {
  val maybeEqual = for {
    rawTokenA <- extractRawToken(tokenA)
    rawTokenB <- extractRawToken(tokenB)
  } yield thetaNTimeEquals(rawTokenA, rawTokenB)
  maybeTrue.getOrElse(false)
}

Почему это работает

При использовании подписанного токена злоумышленник все еще может определить, какой необработанный токен использует уязвимость BREACH, однако, поскольку приложение не принимает необработанные токены, это не полезно для злоумышленника. Поскольку у злоумышленника нет секрета, используемого для подписи подписанного токена, он не может самостоятельно создать подписанный токен из необработанного токена. Следовательно, им необходимо определить не только необработанный токен, но и весь подписанный токен. Но поскольку подписанные токены являются случайными для каждого ответа, это нарушает 3-е предварительное условие выше, что секреты в ответе должны быть статическими, поэтому они не могут выполнять посимвольное вычисление с использованием уязвимости BREACH.

Зашифрованные токены

Другой вариант – зашифровать связанный одноразовый номер и необработанный токен. Это может привести к более коротким токенам, и я не знаю каких-либо существенных различий в производительности между HMAC и AES для этой цели. API для подписи HMAC, как правило, безопаснее в использовании, чем API для шифрования AES, поэтому я использовал подпись HMAC в качестве основного примера.

Рамочные соображения

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

Некоторые платформы, такие как Play Framework, имеют доступный секрет для всего приложения, и поэтому это решение практично реализовать в механизмах защиты на основе маркеров, таких как защита CSRF.