В экосистеме JOSE (JSON Object Signing and Encryption) заголовки играют ключевую роль: они описывают алгоритмы, параметры ключей, идентификаторы и другие метаданные, необходимые для обработки токена. В спецификациях JWS (подпись) и JWE (шифрование) предусмотрено разделение заголовков на три категории:
Unprotected-заголовки представляют собой обычный JSON-объект, который передаётся вместе с токеном, но не участвует в вычислении подписи или аутентифицированного шифрования. Это означает, что они могут быть изменены без нарушения криптографической целостности токена.
Библиотека jose (Node.js и браузер) поддерживает работу
с 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-заголовок, не нарушив подпись. Если система доверяет этим данным, это приводит к атакам:
{
"kid": "attacker-key"
}
Если kid используется для выбора ключа проверки,
возникает риск подмены ключа.
Приложения иногда используют заголовки для маршрутизации или выбора алгоритма. Если такие параметры находятся в unprotected-заголовке, возможно:
Поскольку unprotected-заголовки — это обычный JSON, они могут содержать:
| Характеристика | Protected Header | Unprotected Header |
|---|---|---|
| Участвует в подписи | Да | Нет |
| Защищён от изменения | Да | Нет |
| Кодируется (Base64URL) | Да | Нет |
| Безопасен для критичных данных | Да | Нет |
При проверке JWS:
import { flattenedVerify } from 'jose'
const { protectedHeader, unprotectedHeader } =
await flattenedVerify(jws, secretKey)
protectedHeader — гарантированно подлинныйunprotectedHeader — не
гарантируетсяБиблиотека jose не блокирует использование
unprotected-заголовков, но ответственность за их интерпретацию
полностью лежит на разработчике.
Несмотря на риски, unprotected-заголовки могут использоваться в строго контролируемых случаях:
Информация, не влияющая на безопасность:
Если токены:
Иногда значение дублируется:
kid в unprotected-заголовке.setUnprotectedHeader({ kid: 'key-id' })
Если система использует kid для выбора ключа — это
критическая ошибка.
alg
вне protected-заголовкаАлгоритм должен быть всегда защищён, иначе возможна атака downgrade.
Пример опасной практики:
if (unprotectedHeader.role === 'admin') {
// доступ разрешён
}
В JWE структура сложнее:
Пример:
{
"protected": "...",
"unprotected": { "foo": "bar" },
"recipients": [
{
"header": { "kid": "123" },
...
}
]
}
Здесь:
unprotected — общий для всехheader внутри recipients —
индивидуальныйОба типа не защищены, если не включены в AAD (additional authenticated data).
Самый безопасный подход:
const { protectedHeader } = await verify(...)
И не использовать unprotectedHeader вовсе.
Если использование необходимо:
if (unprotectedHeader?.foo !== 'expected') {
throw new Error('Invalid header')
}
Разрешать только строго определённые ключи:
const allowed = ['debug']
.setProtectedHeader({ kid: 'secure-key-id' })
Unprotected-заголовки доступны только в:
Они отсутствуют в Compact Serialization:
header.payload.signature
Это ещё один фактор, ограничивающий их использование.
В большинстве production-систем:
Многие security-гайды рекомендуют:
“Treat unprotected headers as untrusted input”
В JWE можно частично компенсировать отсутствие защиты:
Однако библиотека jose требует явного указания AAD, и
это редко используется.
Unprotected-заголовки следует рассматривать как:
Любое доверие к ним должно быть обосновано и минимизировано.
jose предоставляет доступ, но не гарантирует
безопасность