Unprotected-заголовки и их безопасность

В экосистеме JOSE (JSON Object Signing and Encryption) заголовки играют ключевую роль: они описывают алгоритмы, параметры ключей, идентификаторы и другие метаданные, необходимые для обработки токена. В спецификациях JWS (подпись) и JWE (шифрование) предусмотрено разделение заголовков на три категории:

  • Protected Header — защищён криптографически (входит в подпись или шифрование)
  • Unprotected Header — передаётся в открытом виде и не защищён
  • Per-recipient Unprotected Header (для JWE) — специфичен для получателя и также не защищён

Unprotected-заголовки представляют собой обычный JSON-объект, который передаётся вместе с токеном, но не участвует в вычислении подписи или аутентифицированного шифрования. Это означает, что они могут быть изменены без нарушения криптографической целостности токена.


Использование unprotected-заголовков в библиотеке jose

Библиотека jose (Node.js и браузер) поддерживает работу с unprotected-заголовками, но делает это осознанно ограниченно, поскольку их применение связано с рисками.

Пример создания JWS с unprotected-заголовком

import { FlattenedSign } from 'jose'

const jws = await new FlattenedSign(
  new TextEncoder().encode('payload')
)
  .setProtectedHeader({ alg: 'HS256' })
  .setUnprotectedHeader({ kid: 'key-id-123' })
  .sign(secretKey)

В этом примере:

  • alg защищён и участвует в подписи
  • kid находится в unprotected-заголовке и может быть изменён без нарушения подписи

Почему unprotected-заголовки небезопасны

Главная проблема заключается в отсутствии криптографической защиты. Это создаёт несколько классов уязвимостей:

Подмена параметров

Злоумышленник может изменить unprotected-заголовок, не нарушив подпись. Если система доверяет этим данным, это приводит к атакам:

{
  "kid": "attacker-key"
}

Если kid используется для выбора ключа проверки, возникает риск подмены ключа.

Нарушение логики обработки

Приложения иногда используют заголовки для маршрутизации или выбора алгоритма. Если такие параметры находятся в unprotected-заголовке, возможно:

  • изменение алгоритма (если проверка реализована некорректно)
  • выбор другого обработчика
  • обход политик безопасности

Инъекции и манипуляции

Поскольку unprotected-заголовки — это обычный JSON, они могут содержать:

  • неожиданные поля
  • вредоносные значения
  • конфликтующие параметры

Различие между protected и unprotected заголовками

Характеристика Protected Header Unprotected Header
Участвует в подписи Да Нет
Защищён от изменения Да Нет
Кодируется (Base64URL) Да Нет
Безопасен для критичных данных Да Нет

Поведение библиотеки jose при верификации

При проверке JWS:

import { flattenedVerify } from 'jose'

const { protectedHeader, unprotectedHeader } =
  await flattenedVerify(jws, secretKey)
  • protectedHeader — гарантированно подлинный
  • unprotectedHeaderне гарантируется

Библиотека jose не блокирует использование unprotected-заголовков, но ответственность за их интерпретацию полностью лежит на разработчике.


Безопасные сценарии использования

Несмотря на риски, unprotected-заголовки могут использоваться в строго контролируемых случаях:

Некритичные метаданные

Информация, не влияющая на безопасность:

  • отладочные данные
  • вспомогательные идентификаторы
  • данные для логирования

Внутренние системы

Если токены:

  • не покидают доверенную среду
  • передаются по защищённым каналам
  • обрабатываются строго контролируемыми сервисами

Дублирование данных

Иногда значение дублируется:

  • в protected-заголовке (для безопасности)
  • в unprotected-заголовке (для удобства доступа)

Антипаттерны

Использование kid в unprotected-заголовке

.setUnprotectedHeader({ kid: 'key-id' })

Если система использует kid для выбора ключа — это критическая ошибка.

Передача alg вне protected-заголовка

Алгоритм должен быть всегда защищён, иначе возможна атака downgrade.

Логика авторизации на основе unprotected данных

Пример опасной практики:

if (unprotectedHeader.role === 'admin') {
  // доступ разрешён
}

Взаимодействие с JWE

В JWE структура сложнее:

  • Protected Header
  • Shared Unprotected Header
  • Per-recipient Unprotected Header

Пример:

{
  "protected": "...",
  "unprotected": { "foo": "bar" },
  "recipients": [
    {
      "header": { "kid": "123" },
      ...
    }
  ]
}

Здесь:

  • unprotected — общий для всех
  • header внутри recipients — индивидуальный

Оба типа не защищены, если не включены в AAD (additional authenticated data).


Как избежать уязвимостей

1. Игнорирование unprotected-заголовков

Самый безопасный подход:

const { protectedHeader } = await verify(...)

И не использовать unprotectedHeader вовсе.

2. Явная валидация

Если использование необходимо:

if (unprotectedHeader?.foo !== 'expected') {
  throw new Error('Invalid header')
}

3. Белый список полей

Разрешать только строго определённые ключи:

const allowed = ['debug']

4. Дублирование критичных данных в protected

.setProtectedHeader({ kid: 'secure-key-id' })

Особенности сериализации

Unprotected-заголовки доступны только в:

  • Flattened JSON Serialization
  • General JSON Serialization

Они отсутствуют в Compact Serialization:

header.payload.signature

Это ещё один фактор, ограничивающий их использование.


Практика в реальных системах

В большинстве production-систем:

  • unprotected-заголовки не используются
  • либо полностью игнорируются
  • либо запрещены на уровне схемы

Многие security-гайды рекомендуют:

“Treat unprotected headers as untrusted input”


Взаимосвязь с AAD (Additional Authenticated Data)

В JWE можно частично компенсировать отсутствие защиты:

  • добавить unprotected-заголовки в AAD
  • тем самым включить их в аутентификацию

Однако библиотека jose требует явного указания AAD, и это редко используется.


Итоговая модель угроз

Unprotected-заголовки следует рассматривать как:

  • пользовательский ввод
  • потенциально вредоносные данные
  • источник атак на бизнес-логику

Любое доверие к ним должно быть обосновано и минимизировано.


Рекомендации по проектированию

  • хранить все критичные параметры в protected-заголовке
  • не использовать unprotected-заголовки для выбора ключей
  • не полагаться на них в логике безопасности
  • при необходимости — валидировать и ограничивать

Краткая сводка

  • Unprotected-заголовки не защищены криптографически
  • Их можно изменять без нарушения подписи
  • Использование допустимо только для некритичных данных
  • Библиотека jose предоставляет доступ, но не гарантирует безопасность
  • Основной принцип: не доверять