Безопасно ли внедрять пользовательские заголовки аутентификации?
Пользовательские заголовки аутентификации нужно проверять на уровне байтов до их внедрения шлюзом. Отклоняйте переводы строк, опасные Unicode-символы и дубликаты полей.

Шлюз, который внедряет учётные данные, должен рассматривать HTTP-заголовок как синтаксис протокола, а не как удобное строковое поле. Если агент, экран настроек, импорт из хранилища или шаблон может поместить в это поле неожиданный байт, шлюз отправит запрос с иным смыслом, чем тот, который предполагают экран одобрения, запись аудита или сервис назначения.
Безопасное правило узкое: оставьте каждое поле аутентификации шлюзу, проверяйте итоговые байты непосредственно перед созданием запроса и отклоняйте некорректный ввод вместо его исправления. Так можно перехватить переводы строк, скрытые управляющие символы, похожие Unicode-символы и дублирующиеся имена полей до того, как учётные данные покинут локальное хранилище.
Для границы внедрения нужен байтовый контракт
У пользовательского заголовка аутентификации должен быть более строгий контракт, чем у общего HTTP-парсинга. Общий парсинг должен поддерживать большой и неоднородный интернет. Шлюз, который переносит секрет из хранилища в запрос, выполняет меньшую задачу, поэтому ему следует принимать меньше вариантов.
Сначала разделите четыре сущности, которые команды часто объединяют под одним понятием «значение заголовка»:
- Настроенное имя поля, например
X-Api-Key. - Байты секрета, сохранённые для этого поля.
- Заголовки запроса, переданные агентом для конкретного действия.
- Итоговый список исходящих заголовков, переданный HTTP-клиенту.
У каждой сущности свой владелец. Администратор или разработчик настраивает имя. Хранилище владеет секретом. Агент может запросить HTTP-операцию и передать обычные заголовки приложения. Только шлюз собирает итоговое защищённое поле.
Это важно, потому что агент никогда не должен получать секрет лишь для того, чтобы соединить его с именем заголовка. Такое разделение также предотвращает более тонкую ошибку: агент передаёт x-api-key, а шлюз позже добавляет X-Api-Key. Имена HTTP-полей нечувствительны к регистру, поэтому это два конкурирующих экземпляра одного поля, а не два независимых заголовка. RFC 9110 определяет имена полей как нечувствительные к регистру и описывает поля, допускающие один элемент, как одиночные поля.
Для заголовка с учётными данными явно задайте байтовый контракт:
- Имя использует только набор символов HTTP-токена и хранится в канонической форме для сравнения, в нижнем регистре ASCII.
- Значение представляет собой непустую последовательность печатаемых ASCII-байтов от
0x21до0x7e. - Значение не содержит пробелов, табуляции, CR, LF, NUL и других управляющих байтов.
- Шлюз добавляет ровно один экземпляр защищённого имени.
- Запрос агента, в котором указано защищённое поле, отклоняется. Его нельзя молча редактировать.
Эта политика намеренно отклоняет часть содержимого, допустимого в общих HTTP-полях. В этом и состоит её смысл. API-ключу, bearer-токену или учётным данным провайдера не нужны табуляция, начальный пробел или символ с диакритикой, чтобы безопасно передаваться в пользовательском заголовке. Если формат учётных данных провайдера требует произвольных байтов, закодируйте их до сохранения, используя документированное представление для передачи. Base64url часто применяют, когда протокол требует печатаемый токен. Не придумывайте в шлюзе преобразование с потерей данных.
Самая соблазнительная альтернатива состоит в том, чтобы принимать произвольный текст, удалять пробелы, заменять управляющие символы пробелами и надеяться, что HTTP-библиотека отклонит остальное. Так появляются несколько представлений одного секрета. Интерфейс может показывать одну строку, журнал содержать другую, а библиотека передавать третью. Средство защиты должно один раз решить, допустимо ли действие. Оно не должно превращать действие во что-то другое.
CR, LF и NUL должны отклоняться до создания запроса
Отклоняйте возврат каретки (0x0d), перевод строки (0x0a) и NUL (0x00) в любом месте защищённого значения заголовка. Одиночные CR и LF нужно отклонять так же строго, как знакомую последовательность CRLF.
HTTP/1.1 использует CRLF для разделения строк полей. RFC 9112 также разрешает получателям при некоторых обстоятельствах распознавать одиночный LF. Именно поэтому исходящий шлюз не может считать, что все компоненты ниже отреагируют одинаково. Стандарт запрещает отправителям создавать одиночный CR внутри элементов протокола и объявляет устаревшими исторические свёрнутые строки полей.
RFC 9110 ещё яснее говорит о значениях полей: CR, LF и NUL недопустимы и опасны, поскольку разные реализации могут разбирать их по-разному. Стандарт требует от получателей отклонять их или заменять пробелами перед дальнейшей обработкой. Замена - это правило восстановления для получателя, а не хороший дизайн шлюза. Шлюз знает, что создаёт новый запрос, поэтому должен отклонить неоднозначность, а не унаследовать её.
Рассмотрим значение, которое поступает в сборщик запроса так:
team-secret\r\nX-Approval: bypassed
Корректный валидатор видит два запрещённых байта и отклоняет действие. Слабый валидатор, который ищет только буквальные символы \\r\\n, видит безобидные обратные косые черты. Валидатор, работающий до раскрытия шаблона, может проверить ${TOKEN} и никогда не увидеть раскрытое значение. Валидатор, заменяющий перевод строки пробелом, превращает очевидный отказ в запрос с искажёнными, изменёнными учётными данными.
Полезна только проверка итоговой последовательности байтов, после декодирования и интерполяции, но до того, как HTTP-библиотека примет заголовки. Результат может выглядеть так:
reject header value: field=x-api-key reason=forbidden-byte byte=0x0a offset=11
Это сообщение предназначено для локальной записи действия, а не для внешнего вызывающего кода. Оно указывает настроенное поле и проблемный байт, не копируя значение. Не записывайте заключённую в кавычки, JSON-экранированную, закодированную в base64 или нормализованную форму секрета. Все эти формы остаются секретными данными и часто живут в журналах дольше самого запроса.
Не ограничивайте это правило HTTP/1.1. HTTP/2 передаёт поля в сжатых блоках, а не в текстовых строках, но RFC 9113 всё равно запрещает NUL, LF и CR в любой позиции значения поля. Стандарт специально предупреждает, что непроверенные поля могут привести к контрабанде запросов, когда посредник переводит их в HTTP/1.1. HTTP/3 говорит то же самое о полях, которые позже попадают в текстовый протокол.
Для имён заголовков нужны правила владения ASCII
Отклоняйте имена заголовков с не-ASCII символами. Не нормализуйте их, не транслитерируйте и не пытайтесь решить, что похожее имя «достаточно близко» к защищённому.
Имена HTTP-полей являются токенами. Общее правило исключает пробелы, управляющие символы, разделители и не-ASCII символы. HTTP/2 добавляет транспортное правило: имена должны быть в нижнем регистре и не могут содержать невидимые ASCII-символы, заглавные буквы и байты выше 0x7f. Шлюз может применить ещё более простое правило: проверить настроенное имя как ASCII-токен HTTP, привести его к нижнему регистру по правилам ASCII, а исходное написание сохранить только для отображения.
Операция сравнения должна быть побайтовой. Если защищённое имя - x-api-key, следующие имена, переданные агентом, должны после приведения ASCII к нижнему регистру совпасть с ним:
X-Api-Key
x-api-key
X-API-KEY
Следующие имена нужно полностью отклонять на этапе проверки, а не исправлять:
x-api‐key // Unicode-дефис
x-api-kеy // кириллическая е
x-api-key\n
x api key
Комментарий в этом примере важен. Человек может не заметить разницу между ASCII-дефисом и Unicode-дефисом или между ASCII e и кириллической е. Парсеру не нужно становиться экспертом по типографике. Отклоняйте все не-ASCII имена заголовков, и неоднозначность исчезнет.
Владение отдельно от синтаксиса. Имя может быть синтаксически корректным, но запрещённым, потому что им владеет шлюз. Храните набор защищённых имён в нижнем регистре ASCII. Проверяйте каждое имя заголовка, переданное агентом, относительно этого набора до объединения коллекций заголовков. Затем создавайте исходящий список с нуля:
обычные заголовки агента, прошедшие проверку
+ защищённый заголовок с учётными данными, принадлежащий шлюзу
+ служебные метаданные запроса, принадлежащие шлюзу, если они нужны
Не начинайте со списка агента, заменяя выбранные элементы. Одни HTTP-библиотеки сохраняют повторяющиеся значения, другие объединяют их, а некоторые представляют заголовки как карту, теряя исходные дубликаты. Как только защищённое поле попало не в ту коллекцию, обычной перезаписи уже нельзя доверять.
Нормализация Unicode не должна исправлять ввод
Не нормализуйте Unicode в значениях аутентификации при внедрении. Нормализация оправдана в системах, которые определяют равенство текста, но секрет обычно представляет собой непрозрачную последовательность байтов, смысл которой определяет его издатель.
Формы нормализации Unicode, описанные в Unicode Standard Annex #15, задают преобразования между канонически или совместимо эквивалентными представлениями текста. Это не делает их безопасными для учётных данных. Строки café и café могут выглядеть одинаково, используя разные последовательности кодовых точек. Провайдер может отклонить одну, принять одну, хешировать одну или считать их разными секретами. Шлюз не может угадать, какое поведение выбрал провайдер.
Есть два разных вопроса о нормализации:
Нормализуйте идентификационные поля только при наличии правила протокола
Продукт может определять, как сравнивать имена пользователей, отображаемые подписи или метаданные приложения. Применяйте это правило на границе, где продукт владеет соответствующим смыслом, и сохраняйте понятную запись выбранного представления. Не переносите правило на внедрение HTTP-учётных данных только потому, что оба значения пришли как строки.
С именами заголовков проще. Это идентификаторы протокола, а не пользовательский текст. Отклоняйте не-ASCII ввод и используйте нижний регистр ASCII для сравнения. NFC, NFD, NFKC, NFKD, свёртка регистра Unicode и скелеты похожих символов не имеют места в этом решении.
Никогда не объединяйте защищённые заголовки по похожести Unicode
Unicode Technical Standard #39 содержит данные о похожих символах для проверок безопасности. В нём прямо сказано, что отображения скелетов похожести не должны превращаться в нормализацию идентификаторов. Это разумное предупреждение: отображение, помогающее заметить подозрительную подпись, не является безопасным правилом для молчаливого изменения протокольного ввода.
При желании используйте обнаружение похожих символов в интерфейсе настроек, чтобы предупредить человека о странной подписи. Не используйте его на пути запроса. Путь запроса должен отвечать однозначно: имя поля является ASCII и зарезервировано либо недопустимо. Значение состоит из печатаемых ASCII-символов и точно соответствует контракту либо недопустимо.
Проверка нормализации Unicode всё равно нужна, даже при политике ASCII для учётных данных. Она выявляет скрытые преобразования в интерфейсах и слоях хранения. Передайте разложенное значение, составной эквивалент, неразрывный пробел, символ нулевой ширины и управляющий символ направления текста через каждый путь ввода. Для пользовательского значения аутентификации правильный результат - отклонение с одной общей категорией: не-ASCII байт или символ. Результат не должен зависеть от того, пришёл ли текст с экрана настроек, из импортированного файла или как аргумент агента.
Дублирующиеся поля означают ошибку авторизации
Для защищённого имени аутентификации дублирующееся поле должно отклонять действие, даже если оба значения совпадают. Один владелец поля проще проверять, одобрять и гораздо меньше зависеть от поведения сервиса назначения.
Часто утверждают, что одинаковые дубликаты безвредны. Но это рассуждение об очистке данных, а не об авторизации. Дубликат может появиться до или после того, как прокси изменит порядок полей. Сервис назначения может выбрать первый экземпляр, последний, объединить экземпляры, отклонить запрос или применить специальное правило для поля. Посредник может сделать другой выбор. В таком случае одобрение действия больше не описывает один однозначный запрос.
Не решайте эту проблему заявлением, что Authorization особенный, а любой заголовок X-... можно считать обычным. Поставщик может определить собственное поле учётных данных как допускающее одно значение, и большинство схем аутентификации так и поступает. Шлюз должен хранить явный список защищённых имён для каждого соединения или привязки учётных данных. В список могут входить authorization, x-api-key, x-api-token или настроенное разработчиком поле поставщика. Список не должен зависеть от соглашения об именовании.
Проверяйте дубликаты до того, как HTTP-клиент преобразует список заголовков в собственное представление. Многие удобные API используют карту, где нечувствительному к регистру имени соответствует список строк. При аккуратном использовании это сохраняет дубликаты, но может скрывать порядок и источник владения. Другие API предлагают метод set, заменяющий значение, и метод add, добавляющий ещё одно. Эти операции допустимы только после того, как шлюз отклонил попытку агента затронуть защищённое имя.
Используйте эту таблицу решений для каждого заголовка, переданного агентом:
| Условие | Результат шлюза |
|---|---|
| Недопустимое имя поля | Отклонить действие |
| Защищённое имя в любом регистре ASCII | Отклонить действие |
| Дубликат обычного имени, если протокол назначения его допускает | Сохранить только по явному правилу для конкретного имени |
| Дубликат обычного имени с неизвестной семантикой | Отклонить действие или потребовать правило для конкретного соединения |
| Поле учётных данных шлюза | Добавить ровно один раз после проверки заголовков агента |
Третья строка намеренно узкая. У Accept может быть семантика списка. У пользовательского поля приложения её может не быть. Если шлюз не знает, меняет ли повторение смысл, ему не следует выдумывать правило. Именно здесь общая функция «передать все заголовки» незаметно превращается в способ обойти намерение человека.
Проверяйте каждую границу преобразования, затем итоговый результат
Одной ранней проверки недостаточно, поскольку значения запроса часто меняют форму после неё. Каждая граница преобразования, на которой могут создаваться, декодироваться, объединяться или подставляться байты, нуждается в локальном инварианте, а итоговый сборщик запроса должен выполнить решающую проверку.
Практический поток имеет пять границ:
- При настройке имени заголовка проверьте синтаксис ASCII-токена и зарезервируйте его форму в нижнем регистре.
- При попадании секрета в хранилище проверьте контракт учётных данных и сохраните точные одобренные байты.
- Когда агент передаёт операцию, проверьте имена и значения обычных заголовков запроса, затем отклоните защищённые имена.
- После раскрытия шаблонов, чтения файлов, подстановки переменных окружения и декодирования структурированного запроса снова проверьте значения, поскольку эти процессы могли создать новые байты.
- Непосредственно перед вызовом HTTP-библиотеки проверьте полный список полей и убедитесь, что каждое защищённое имя встречается один раз.
Итоговая проверка имеет приоритет, потому что видит фактический исходящий объект. Ранние проверки нужны для понятных сообщений об ошибках и для защиты от попадания плохого состояния в систему. Они её не заменяют.
Именно поэтому проверка должна находиться в ядре шлюза, а не в описании MCP-инструмента, подсказке или вспомогательном компоненте агента. Эти места могут описать предполагаемый запрос, но не гарантируют, что попадёт в сокет. Узел, владеющий внедрением учётных данных, должен владеть последней проверкой байтов.
Sallyport реализует полезную часть этой границы: приложение само выполняет внедрение HTTP-учётных данных, а агент получает результат, а не секрет. Для учётных данных в пользовательском заголовке слой действий всё равно должен отклонять копию зарезервированного поля, переданную агентом, до того, как она достигнет точки внедрения.
Ошибки проверки должны быть детерминированными. Если запрос содержит CR в байте 8 и дублирующийся защищённый заголовок, сообщайте о первой ошибке по фиксированному порядку, например: недопустимое имя, конфликт защищённого имени, недопустимое значение, затем итоговое количество. Детерминированность делает тесты надёжными и не позволяет атакующему использовать различия в ошибках для получения сведений о сохранённых учётных данных.
Сделайте валидатор достаточно маленьким для аудита
Короткий валидатор с жёстким контрактом безопаснее общего санитайзера с длинным списком исключений. Следующий пример на Go намеренно разрешает в пользовательском значении аутентификации только печатаемые ASCII-байты. Он не удаляет пробелы, не нормализует, не декодирует и не переписывает ввод.
package authheader
import (
"fmt"
"strings"
)
func canonicalName(name string) (string, error) {
if name == "" {
return "", fmt.Errorf("empty field name")
}
for i := 0; i < len(name); i++ {
b := name[i]
ok := b == '!' || b == '#' || b == '$' || b == '%' || b == '&' ||
b == '\'' || b == '*' || b == '+' || b == '-' || b == '.' ||
b == '^' || b == '_' || b == '`' || b == '|' || b == '~' ||
('0' <= b && b <= '9') || ('A' <= b && b <= 'Z') ||
('a' <= b && b <= 'z')
if !ok {
return "", fmt.Errorf("invalid field-name byte 0x%02x at offset %d", b, i)
}
}
return strings.ToLower(name), nil
}
func validateCredentialValue(value string) error {
if len(value) == 0 {
return fmt.Errorf("empty credential value")
}
for i := 0; i < len(value); i++ {
b := value[i]
if b < 0x21 || b > 0x7e {
return fmt.Errorf("invalid credential byte 0x%02x at offset %d", b, i)
}
}
return nil
}
func rejectProtected(headers [][2]string, protected map[string]struct{}) error {
for _, h := range headers {
name, err := canonicalName(h[0])
if err != nil {
return err
}
if _, found := protected[name]; found {
return fmt.Errorf("agent supplied protected field %q", name)
}
}
return nil
}
Этот пример разрешает : в значении, поскольку печатаемый ASCII включает его. Для многих API-токенов это нормально. Если сервис назначения принимает только документированное подмножество, применяйте правило провайдера в валидаторе для конкретного соединения. Не заставляйте глобальную политику шлюза считать двоеточие, косую черту или знак равенства подозрительными.
Использование string в Go не означает, что валидатор стал работать с Unicode. При индексации строки Go возвращает байты. Многобайтовая последовательность UTF-8 содержит байты выше 0x7e, поэтому этот контракт корректно её отклоняет. Если среда выполнения хранит скалярные значения Unicode, а не строки байтов, сначала закодируйте их в точное исходящее представление, затем выполните проверку байтов.
Вызывающий код не должен предоставлять validateCredentialValue как средство очистки. Единственные допустимые результаты - одобрение неизменённых байтов или ошибка. К любому методу с именем sanitizeHeader, cleanHeader или normalizeToken стоит отнестись с подозрением при проверке: такие названия подталкивают вызывающий код преобразовывать секрет, полагая, что он его защищает.
В матрице тестов нужны враждебные представления, а не только опасные строки
Набор тестов должен доказывать, что каждый путь в исходящий запрос приводит к одному и тому же итоговому валидатору. Недостаточно напрямую вызвать валидатор с очевидным CRLF.
Сначала используйте табличные модульные тесты для необработанных значений:
cases := []struct {
name string
value string
want bool
}{
{"ordinary token", "mF_9.B5f-4.1JqM", true},
{"carriage return", "abc\rdef", false},
{"line feed", "abc\ndef", false},
{"crlf", "abc\r\ndef", false},
{"nul", "abc\x00def", false},
{"leading space", " abc", false},
{"trailing tab", "abc\t", false},
{"composed Unicode", "caf\u00e9", false},
{"decomposed Unicode", "cafe\u0301", false},
}
Затем проверьте полный путь действия на примерах, которые выглядят обычными для прикладного кода:
- JSON-запрос, в котором перевод строки появляется после декодирования
\n. - Импорт конфигурации, где файл заканчивается переводом строки после токена.
- Результат шаблона, в котором отсутствующая переменная превращается в пустую строку.
- Запрос агента одновременно с
X-Api-Keyиx-api-key. - Запрос агента с похожим не-ASCII символом в имени, который должен завершиться ошибкой синтаксиса имени, а не обойти проверку защищённого имени.
Случай с концом файла выявляет распространённую привычку. Разработчик копирует токен в текстовый файл, незаметно добавляет завершающий перевод строки и использует подстановку команды или импортёр, который его сохраняет. В одном пути shell-команда может удалить конечные переводы строк, а в другом чтение файла их сохранит. Правильное поведение шлюза одинаково: он не удаляет пробелы. Он просит оператора исправить сохранённые учётные данные.
Добавьте тесты свойств для итогового сборщика. Генерируйте короткие строки байтов, содержащие каждый управляющий байт, байты выше 0x7f и печатаемый ASCII. Утверждайте, что проходят только непустые строки, полностью лежащие в разрешённом диапазоне. Генерируйте варианты регистра каждого защищённого имени и проверяйте, что каждый вариант отклоняет попытку агента владеть полем. Такие тесты находят ошибки вроде проверки только общей последовательности \r\n, забытых одиночных LF или приведения к нижнему регистру после поиска в защищённом наборе.
Наконец, используйте интеграционный обработчик, который перехватывает заголовки, фактически получаемые HTTP-библиотекой. Он никогда не должен увидеть некорректное защищённое значение, поскольку шлюз обязан отклонить его раньше. Обработчик проверяет создание запроса, а не политику безопасности. Если интеграционный тест обнаруживает второе защищённое поле, считайте это дефектом шлюза, даже если сервер назначения отклонил бы запрос.
Разрешительный запасной вариант скрывает ошибку, которую нужно исправить
Замена запрещённых байтов пробелами, удаление пробелов или сохранение «последнего» дубликата популярны потому, что помогают демонстрациям работать. Но они затрудняют восстановление источника исходящих учётных данных.
Рассмотрим удаление пробелов. Оператор импортирует из файла token-42\n. Один путь удаляет перевод строки и отправляет token-42. Другой сохраняет исходное значение и позже отклоняет его. Теперь оператор видит, что действие работает в локальном тесте, но не работает в шлюзе. Рано или поздно кто-то добавит удаление пробелов в шлюз, чтобы устранить несоответствие. Через несколько месяцев шаблон выдаёт token-42\r\nX-Role: admin; тот же код может удалить только конечный перевод строки и оставить внутренний CRLF. «Полезное» поведение скрыло и источник ошибки, и границу безопасности.
Правильное исправление скучно. Определите один допустимый формат учётных данных, отклоняйте все остальные значения и объясняйте инструментами импорта, почему байты не подходят. Экран настроек может показать trailing LF at offset 8, не раскрывая токен. Импортёр командной строки может вывести ту же категорию и завершиться с ненулевым кодом. Пользователь исправляет источник, вместо того чтобы каждый следующий слой пытался угадывать.
Записи аудита должны отличать отказ в авторизации от сбоя транспорта. Запишите, что процесс агента запросил действие, что действие было отклонено до отправки, какая настроенная привязка учётных данных использовалась и какова категория проверки. Не обращайтесь к сервису назначения и не создавайте искусственный HTTP-ответ, похожий на удалённый отказ. Удалённого запроса не было.
Отдельные записи сеансов и активности Sallyport дают такому отказу полезное место: сеанс показывает, какой запуск агента его инициировал, а запись действия может показать, что локальная проверка остановила отправку. Запись должна оставаться полезной, не сохраняя отклонённый секрет или его экранированное приближение.
Правило строгое, потому что шлюз владеет секретом
Обычный HTTP-клиент может принимать широкий ввод, поскольку часто отправляет данные, которыми вызывающий код уже владеет. У шлюза учётных данных другая ответственность. Он решает, может ли агент вызвать действие с секретом, и должен принять это решение для запроса, синтаксис которого не может измениться после проверки.
Отклоняйте управляющие символы и не-ASCII в значениях пользовательских учётных данных. Резервируйте имена аутентификации и сравнивайте их по правилам нижнего регистра ASCII. Отклоняйте дублирующиеся защищённые поля. Никогда не нормализуйте и не удаляйте пробелы из учётных данных по пути наружу. Проверяйте каждый путь ввода, затем ещё раз проверяйте итоговый список заголовков.
Если формат аутентификации провайдера не помещается в этот контракт, задокументируйте специфичное для провайдера кодирование и применяйте его до того, как секрет достигнет сборщика запроса. Не ослабляйте шлюз, разрешая неоднозначный текст, только потому, что одна интеграция принесла неаккуратный токен. Первый некорректный байт должен остановиться на границе, а отказ должен объяснить проблему с байтами, не раскрывая учётные данные.
Вопросы и ответы
Должен ли шлюз отклонять и CRLF, и одиночный перевод строки в значении HTTP-заголовка?
Отклоняйте оба варианта. Возврат каретки и перевод строки могут изменить то, как получатель HTTP/1.1 воспринимает границы полей, а разные получатели исторически по-разному обрабатывали окончания строк. Не исправляйте ни один из этих символов в заголовке с учётными данными. Отклоняйте действие до создания исходящего запроса.
Нужно ли блокировать байты NUL в пользовательских заголовках аутентификации?
NUL не имеет законного места в значении заголовка аутентификации, а HTTP считает его недопустимым. Отклоняйте его вместе с CR и LF, а в событии аудита записывайте смещение байта и роль поля, не сохраняя сам секрет.
Нужно ли нормализовать API-ключи Unicode перед добавлением в заголовок?
Не нормализуйте учётные данные при внедрении. Нормализация Unicode может сделать две строки равными при сравнении, изменив байты, которых ожидает провайдер. Храните и внедряйте точно одобренные байты либо требуйте ASCII-кодирование транспорта, например base64url, до помещения секрета в хранилище.
Как безопасно сравнивать имена дублирующихся HTTP-заголовков?
Рассматривайте имена полей заголовка как ASCII-токены протокола, затем сравнивайте их формы в нижнем регистре ASCII. Не применяйте к именам полей свёртку регистра Unicode или NFKC. Имя с не-ASCII символами должно не исправляться, а отклоняться.
Должен ли шлюз удалять или отклонять дублирующийся заголовок Authorization?
Для внедряемого поля аутентификации отклоняйте запрос. Тихое удаление копии, переданной агентом, скрывает ошибку авторизации и может оставить разные уровни системы с разными представлениями о произошедшем. У защищённого поля должен быть один владелец, шлюз.
Безопасно ли разрешать табуляцию или Unicode в значении заголовка API-ключа?
Общий HTTP-парсер может разрешать более широкий набор символов, чем должен принимать контракт учётных данных. Для пользовательской аутентификации обычно безопаснее и проще тестировать политику видимых ASCII-символов без начальных и конечных пробелов.
Какие тесты выявляют ошибки внедрения HTTP-заголовков?
Проверяйте необработанные CR, LF, CRLF, NUL, начальные и конечные пробелы, табуляции, не-ASCII байты, составной и разложенный Unicode, а также дублирующиеся имена с разным регистром ASCII. Прогоняйте те же случаи через все пути конфигурации, хранилища, шаблонов и создания запроса.
Где должен выполняться контроль заголовков в шлюзе для агентов?
Значение нужно проверять непосредственно перед передачей HTTP-библиотеке, поскольку декодирование, интерполяция или сериализация могут изменить его после ранней проверки. Проверяйте настроенное имя при сохранении, но для каждого исходящего действия проверяйте итоговую последовательность байтов.
Опасны ли переводы строк в заголовках только для HTTP/1.1?
HTTP/2 убирает текстовое кадрирование CRLF, используемое HTTP/1.1, но по-прежнему запрещает CR, LF и NUL в значениях полей. Шлюз также может пересекать границы протоколов через прокси, поэтому проверка не должна зависеть от версии транспорта.
Что должен записывать журнал аудита при ошибке проверки значения заголовка?
Записывайте, что действие отклонено, настроенное имя защищённого поля, категорию ошибки и позицию проблемного байта. Не записывайте переданное значение, нормализованную копию или экранированное представление учётных данных. Escape-нотация часто становится ещё одним каналом утечки секрета.