• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Злоупотребляем косяками парсинга JSON, XML и YAML в программах на Go

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,167
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Парсинг ненадежных данных создает идеальную лазейку для атак на приложения, написанные на Go. В ходе наших исследований безопасности мы не раз находили уязвимости в парсерах JSON, XML и YAML Go, которые позволяли обходить аутентификацию, нарушать авторизационные правила и выкачивать чувствительные данные прямо с рабочих систем.
Это близкий к тексту пересказ статьи «Unexpected security footguns in Go’s parsers» из блога Trail of Bits. Ее автор — Васко Франко. Материал доступен без платной подписки.
Это не просто теоретические баги — они уже привели к реальным уязвимостям, таким как Для просмотра ссылки Войди или Зарегистрируйся (обход аутентификации в HashiCorp Vault, который обнаружили ребята из Google Project Zero), и множеству критических находок в проектах наших клиентов.


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

  1. (Де)сериализация неожиданных данных: как парсеры на Go могут раскрыть данные, которые разработчики планировали оставить приватными.
  2. Различия парсеров: как несовпадения в работе разных парсеров позволяют хакерам обходить меры безопасности, когда несколько сервисов обрабатывают одинаковые данные.
  3. Путаница в форматах данных: как парсеры обрабатывают межформатные данные с неожиданными и порой весьма взрывоопасными результатами.
Мы покажем каждый сценарий атаки на примере из реальной жизни и завершим все конкретными рекомендациями о том, как безопаснее использовать парсинг. В том числе расскажем, как затыкать дыры в безопасности стандартной библиотеки Go.

Вот краткий обзор неожиданных поведений, которые мы рассмотрим, с индикаторами.

ОсобенностьJSONJSON v2XMLYAML
json:"-,"Да
(плохой дизайн)
Да
(плохой дизайн)
Да
(плохой дизайн)
Да
(плохой дизайн)
json:"omitempty"Да
(как и ожидалось)
Да
(как и ожидалось)
Да
(как и ожидалось)
Да
(как и ожидалось)
Дублирующиеся ключиДа
(последний)
НетДа
(последний)
Нет
РегистронезависимостьДаНетНетНет
Неизвестные ключиДа
(исправимо)
Да
(исправимо)
ДаДа
(исправимо)
Лишние ведущие данныеНетНетДаНет
Лишние данные в концеДа
(с Decoder)
НетДаНет

Парсинг в Go​

Давай разберем, как Go обрабатывает JSON, XML и YAML. В стандартной библиотеке Go найдутся парсеры для JSON и XML, а вот для YAML придется подобрать что‑то стороннее — на выбор есть куча реализаций. В этой статье мы сосредоточимся на таких парсерах:

В наших примерах будем использовать JSON, но у всех трех парсеров одинаковые API, так что разницы почти никакой.

В своей основе эти парсеры выполняют две главные функции:

  • Marshal (сериализация) — превращает структуры Go в строки заданного формата;
  • Unmarshal (десериализация) — конвертирует строки формата обратно в структуры Go.
Для просмотра ссылки Войди или Зарегистрируйся
В Go есть фишка — теги полей структур, позволяющие настроить, как парсеры должны работать с отдельными полями. Вот из чего состоят эти теги:

  • имя ключа для сериализации/десериализации;
  • опциональные директивы через запятую для изменения поведения (например, опция omitempty в JSON указывает сериализатору пропустить пустое поле в строке вывода).
Код:
type User struct {
    // Поле для имени пользователя в JSON
    Username string json:"username_json_key,omitempty"
    // Пароль пользователя
    Password string json:"password"
    // Флаг для админа
    IsAdmin  bool   json:"is_admin"
}

Чтобы распарсить JSON-строку в структуру User, описанную выше, тебе нужно использовать ключ username_json_key для поля Username, password для поля Password, а для поля IsAdmin — ключ is_admin.

Код:
u := User{}
_ = json.Unmarshal([]byte(`{
    "username_json_key": "jofra",
    "password": "qwerty123!",
    "is_admin": "false"
}`), &u)
fmt.Printf("Result: %#v\n", u)
// Результат: User{Username:"jofra", Password:"qwerty123!", IsAdmin:false}

Эти парсеры также поддерживают потоковые методы, работающие с интерфейсами io.Reader вместо byte-срезов. Такой API особенно хорош для парсинга потоковых данных, например тел HTTP-запросов, и поэтому часто применяется в системах обработки HTTP.

Парсинг JSON в Go с помощью NewDecoder

Атака 1: подмена данных при (де)сериализации​

Иногда нужно ограничить, какие именно поля структуры могут быть сериализованы или десериализованы.

Давай разберем простой пример: есть бэкенд‑сервер, на котором настроен HTTP-хендлер для создания пользователей и еще один — для их извлечения после аутентификации.

Когда создаешь пользователя, возможно, тебе не захочется, чтобы он мог задать поле IsAdmin сам (то есть парсить это поле из пользовательского ввода).

Взаимодействие с бэкенд‑сервером, где пользователь может изменить поле IsAdmin в структуре User, — такую возможность давать нельзя

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

Взаимодействие с сервером, где пользователь каким‑то образом получает доступ к полю Password в структуре User, хотя это должно быть невозможно

Как можно указать парсерам не сериализовать или десериализовать поле?


Поля без меток​

Давай сначала посмотрим, что произойдет, если ты не установишь тег JSON.

Код:
type User struct {
    Username string
}

В этом случае ты можешь распаковать поле Username по его имени, как показано ниже.

Код:
_ = json.Unmarshal([]byte({"Username": "jofra"}), &u)
// Результат: User{Username:"jofra"}

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

Код:
type User struct {
    // Имя пользователя, если не заполнено — не отображается в JSON
    Username string json:"username,omitempty"
    // Пароль пользователя, тоже исчезающий в JSON, если пустой
    Password string json:"password,omitempty"
    // Флаг того, что пользователь — админ
    IsAdmin  bool
}

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

Если хочешь просканировать свой код и обнаружить ситуации, когда только часть полей имеет JSON-, XML- или YAML-теги, используй следующее Semgrep-правило. Оно не попало в наш Для просмотра ссылки Войди или Зарегистрируйся, так как в зависимости от твоего кода велика вероятность получить кучу ложных срабатываний.

Код:
rules:
    - id: unmarshaling-tag-in-only-some-fields
      message: >-
          Тип $T1 имеет поля с тегами json/yml/xml только на некоторых полях, а не на всех. Такое поле все равно может быть (де)сериализовано по имени. Чтобы предотвратить (де)сериализацию поля, используй тег -.
      languages: [go]
      severity: WARNING
      patterns:
          - pattern-inside: |
                type $T1 struct {
                  ...
                  $_ $_ $TAG
                  ...
                }
          # Этот регекс пытается избежать некоторых ложных срабатываний, таких как структуры, объявленные внутри структур
          - pattern-regex: >-
                ^[ \t]+[A-Z]+[a-zA-Z0-9][ \t]+[a-zA-Z0-9]+[^{`\n\r]$
          - metavariable-regex:
                metavariable: $TAG
                regex: >-
                    .*(json|yaml|xml):"[^,-

Неправильное использование тега -​

Чтобы заставить парсер игнорировать конкретное поле при (де)сериализации, добавь спецтег JSON, который выглядит как знак минуса.
Код:
type User struct {
    // Имя пользователя в JSON
    Username string json:"username,omitempty"
    // Пароль в JSON
    Password string json:"password,omitempty"
    // Админские права не добавляются в JSON
    IsAdmin  bool   json:"-,omitempty"
}
Поехали!

Код:
_ = json.Unmarshal([]byte({"-": true}), &u)
// Результат: main.User{Username:"", Password:"", IsAdmin:true}

Вот незадача, мы все‑таки смогли установить поле IsAdmin. Мы по ошибке скопировали часть ,omitempty, из‑за чего парсер начал искать ключ - в предоставленном JSON. Я пробежался по топ-1000 репозиториев на Go на GitHub с наибольшим количеством звезд и среди прочих нашел два таких случая (и отрапортовал о них, так что их уже исправили):

  • в Flipt поле ClientID в конфигурации OIDC отображалось как - (исправлено в Для просмотра ссылки Войди или Зарегистрируйся);
  • в langchaingo поле MaxTokens также отображалось как - (исправлено в Для просмотра ссылки Войди или Зарегистрируйся).
Это поведение чревато ошибками и не приносит особой пользы (ну кроме возможности назвать поле -), и тем не менее оно описано в Для просмотра ссылки Войди или Зарегистрируйся.

info​

Особый случай: если у тега поля стоит -, то это поле всегда пропускается. Фокус в том, что поле с именем - все еще можно создать, используя тег -,.
Парсеры для XML и YAML работают похоже, но есть один подвох: XML-парсер считает тег <-> некорректным. Чтобы это исправить, нужно добавить для символа «минус» пространство имен. Например, превратить его в <A:->.

Распаковываем поле с тегом - в форматах JSON, XML и YAML

Хорошо, давай на этот раз все сделаем правильно.

Код:
type User struct {
    // Имя пользователя в JSON, если не пустое
    Username string  json:"username,omitempty"
    // Пароль в JSON, если не пустой
    Password string  json:"password,omitempty"
    // Не отображать признак админа в JSON
    IsAdmin  bool    json:"-"
}

Наконец‑то! Теперь нет никакой возможности для десериализации поля IsAdmin.

Ты, наверное, спросишь: как же эти неправильные настройки могут превратиться в уязвимости в безопасности? Самый банальный способ — это, как в нашем примере, использовать -,... как JSON-тег для поля типа IsAdmin, которое пользователь ни в коем случае не должен контролировать.

Детектить такую штуку обычными юнит‑тестами очень сложно, ведь без специального теста, который распаковывает данные с ключом - и проверяет, изменилось ли поле, ты такой баг не поймаешь. Тут нужны либо какие‑то продвинутые фичи IDE, либо сторонний инструмент.

Взаимодействие с сервером, где пользователь может установить поле IsAdmin с помощью поля JSON

Мы разработали Для просмотра ссылки Войди или Зарегистрируйся, которое поможет тебе выявить подобные проблемы в коде. Зацени его в деле:

semgrep -c r/trailofbits.go.unmarshal-tag-is-dash

Ошибки с omitempty​

Еще одна простая, но веселая ошибка конфигурации, с которой мы сталкивались раньше: разработчик по ошибке задал имя поля как omitempty.

// Результат: User{Username:"a_user"}
Если ты установишь для JSON-тега omitempty, то парсер будет использовать omitempty как имя поля (что ожидаемо). Некоторые разработчики пытались так хитрить: ставить omitempty в качестве опции и при этом оставлять стандартное имя поля. Я пошерстил топ-1000 репозиториев на Go в поисках такого трюка, и вот что удалось нарыть:

Как видишь, разработчики часто хотят установить тег в значение json:",omitempty", чтобы сохранить имя по умолчанию и добавить опцию omitempty.

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

semgrep -c r/trailofbits.go.unmarshal-tag-is-omitempty

Атака 2: разные парсеры — разные результаты​

Что произойдет, если разобрать одни и те же данные с помощью разных JSON-парсеров и они выдадут разные результаты? И самое интересное — какие особенности парсеров в Go дают злоумышленникам возможность стабильно провоцировать такие расхождения?

Возьмем для примера приложение, построенное на микросервисной архитектуре. В его составе:

  • прокси‑сервис, который обрабатывает все запросы пользователей;
  • сервис авторизации, на который прокси‑сервис ссылается, чтобы проверить, есть ли у пользователя нужные права для выполнения его запроса;
  • набор сервисов бизнес‑логики, которые прокси‑сервис дергает, чтобы реализовать бизнес‑логику.
В этом сценарии обычный пользователь без прав админа пытается выполнить UserAction — действие, которое ему разрешено выполнять.

Пользователь успешно проходит аутентификацию

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

Пользователь пытается войти, но аутентификация проваливается

И вот происходит магия: сервисы начинают спорить о том, что же ты все‑таки пытаешься сделать.

Службы Proxy и Authorization не согласны в разборе пользовательского ввода, что создает уязвимость в потоке данных

Сервис авторизации, который написан на другом языке программирования или использует нестандартный парсер для Go, будет парсить UserAction и даст пользователю права на выполнение операции. А вот прокси‑сервис, который использует стандартный парсер Go, разберет AdminAction и отправит его не тому сервису. Остается вопрос: какие нагрузки мы можем создать, чтобы добиться такого поведения?

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


Дубли полей​

Первая уязвимость, которую мы разберем, — это дублирование ключей. Что будет, если во входном JSON один и тот же ключ встречается дважды? А тут уже все зависит от парсера!

В Go парсер JSON всегда возьмет последний элемент. И такую логику никак не поменять.

Код:
_ = json.Unmarshal([]byte(`{
    "action": "Action1",
    "action": "Action2"
}`), &a)
// Итог: ActionRequest{Action:"Action2"} — последний ключ побеждает!

Это стандартное поведение большинства парсеров. Но, как Для просмотра ссылки Войди или Зарегистрируйся ребята из Bishop Fox, 7 из 49 протестированных парсеров выбирают первый ключ:

  • Go: jsonparser, gojay;
  • C++: rapidjson;
  • Java: json-iterator;
  • Elixir: Jason, Poison;
  • Erlang: jsone.
Ни один из них не является самым популярным JSON-парсером для своего языка, хотя некоторые из них весьма распространенные.

Итак, если наш Proxy Service использует JSON-парсер на Go, а Authorization Service — один из перечисленных парсеров, мы получаем расхождение, как показано на картинке.

Сценарий атаки с использованием дублирующихся полей

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

Для просмотра ссылки Войди или Зарегистрируйся
Хотя это и не идеально, но такое поведение по крайней мере соответствует большинству используемых парсеров JSON и XML. Теперь давай посмотрим на более серьезную проблему, которая почти всегда приводит к расхождениям между парсером Go по умолчанию и любым другим парсером.


Поиск по ключам без учета регистра​

Парсер JSON в Go распознает имена полей без учета регистра. Пиши action как action, ACTION или aCtIoN — для парсера это все одно и то же!

Код:
_ = json.Unmarshal([]byte(`{
    "aCtIoN": "Action2"
}`), &a)
// Результат: ActionRequest{Action:"Action2"}

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

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

Код:
_ = json.Unmarshal([]byte(`{
    "action": "Action1",
    "aCtIoN": "Action2"
}`), &a)
// Результат: ActionRequest{Action:"Action2"}

Это противоречит документации, где сказано:

Ты можешь даже использовать символы Unicode! В примере ниже мы используем символ ſ (латинское долгое s) как s и K (знак Кельвина) как k. В нашем тестировании библиотеки JSON, которая выполняет сравнение, только эти два символа Unicode соответствуют ASCII-символам.

Код:
type ActionRequest struct {
    Action string json:"aktions"
}
a := ActionRequest{}
_ = json.Unmarshal([]byte(`
{
    "aktions": "Action1",
    "aKtionſ": "Action2"
}
`), &a)
fmt.Printf("Result: %#v\n", a)
// Результат: main.ActionRequest{Action:"Action2"}

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

Сценарий атаки

На наш взгляд, это самый жесткий косяк JSON-парсера в Go, поскольку поведение отличается от поведения парсеров в JavaScript, Python, Rust, Ruby, Java и всех остальных, которые мы тестировали. В результате образовалась куча серьезных уязвимостей, включая те, что нам удалось вскрыть в ходе аудитов.

И последний штрих: отключить это поведение нельзя, несмотря на то что пользователи жалуются на Для просмотра ссылки Войди или Зарегистрируйся уже с 2016 года.

Это касается только парсера JSON. Парсеры для XML и YAML используют точные совпадения.

Изображение, обобщающее работу всех трех парсеров

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


Атака 3: путаница в форматах данных​

Для финальной атаки давай посмотрим, что будет, если распарсить JSON-файл XML-парсером или использовать какой‑нибудь другой неподходящий формат.

Возьмем для примера Для просмотра ссылки Войди или Зарегистрируйся: байпас защиты HashiCorp Vault в методе аутентификации через AWS IAM. Эту уязвимость обнаружила команда Google Project Zero (если интересно погрузиться в детали, в блоге есть пост под названием Для просмотра ссылки Войди или Зарегистрируйся). Мы не будем вываливать здесь все тонкости, но в целом вот как выглядит стандартный процесс аутентификации HashiCorp Vault через AWS IAM:

  1. AWS-ресурс, скажем функция AWS Lambda, подписывает запрос Для просмотра ссылки Войди или Зарегистрируйся.
  2. Этот запрос отправляется на сервер Vault.
  3. Сервер Vault собирает запрос и пересылает его в AWS Security Token Service (STS).
  4. AWS STS проверяет подпись.
  5. Если все окей, AWS STS возвращает XML-документ с данными о роли.
  6. Сервер Vault парсит XML, извлекает идентификатор и, если у этой роли есть доступ к запрашиваемым секретам, отправляет их обратно.
  7. Теперь AWS-ресурс может использовать секреты, например чтобы авторизоваться в базе данных.
Процесс аутентификации с помощью Vault

Команда Google Project Zero обнаружила, что в шаге 2 злоумышленник может получить слишком большой контроль, включая задание всех заголовков запроса, которые Vault формирует на шаге 3. Особенно критично, что, установив заголовок Accept как application/json, AWS STS в шаге 5 вместо ожидаемого XML-документа возвращает JSON.

В итоге сервер Vault начинает парсить этот JSON с использованием XML-парсера на Go. А поскольку XML-парсер весьма снисходителен и умудряется разобрать почти все, что хоть отдаленно смахивает на XML, этот хаос из JSON становится достаточным для обхода аутентификации, если есть возможность хотя бы частично управлять JSON-ответом.

Процесс аутентификации Vault и уязвимость

Давай разберем три хитрости, позволяющие парсить файлы некорректным парсером Go, и создадим файл‑полиглот, который можно скормить парсерам JSON, XML и YAML. Каждому парсеру он выдаст свой уникальный результат.


Неизвестные ключи​

По умолчанию парсеры для JSON, XML и YAML не блокируют неизвестные поля — то есть свойства во входящих данных, которые не соответствуют ни одному полю в целевой структуре.

Поведение парсеров JSON, XML и YAML при обработке неизвестных ключей

Мусорные данные в начале​

Из трех парсеров только XML-парсер переваривает мусорные данные в начале.

Поведение парсеров JSON, XML и YAML при наличии мусора в начале данных

Мусорные данные в конце​

Только XML-парсер может проглотить произвольный мусор в конце данных.

Поведение парсеров JSON, XML и YAML при столкновении с мусорными данными в конце файла

Исключение составляет использование парсера Decoder API с потоковыми данными — в этом случае JSON-парсер примет мусорные данные в конце. Это Для просмотра ссылки Войди или Зарегистрируйся, для которой пока нет запланированного решения.

Поведение парсеров JSON, XML и YAML при обработке мусора в конце данных с использованием API Decoder

Создаем полиглот​

Как объединить все рассмотренные нами способы поведения, чтобы создать файл‑полиглот, который:

  • можно обработать парсерами JSON, XML и YAML на Go;
  • возвращает разные результаты для каждого парсера?
Пригодится знать, что JSON — это подмножество YAML.

info​

Любой файл JSON — это одновременно и валидный файл YAML!
Имея это в виду, мы можем замутить такой полиглот.

Файл‑полиглот и то, как разные парсеры его интерпретируют

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

Парсер YAML может обработать этот файл, потому что входные данные — это валидный JSON (а значит, и валидный YAML), а неизвестные ключи он просто игнорирует. Он цепляется за значение Action_1, поскольку, в отличие от парсера JSON, делает точное сопоставление по именам полей.

Наконец, парсер XML способен распознать наш формат, потому что игнорирует все лишнее и ищет лишь данные, которые напоминают XML. Мы спрятали их внутри значения JSON. В результате парсер выполняет Action_3.

Это мощная отправная точка для проведения атак на основе путаницы в форматах данных, аналогичных обходу HashiCorp Vault.


Способы защиты​

Как свести риск к минимуму и сделать парсинг JSON более строгим? Мы бы хотели:

  • не допускать разбора неизвестных ключей в JSON, XML и YAML;
  • не допускать разбора дублирующихся ключей в JSON и XML;
  • исключить учет регистра ключей в JSON (это особенно важно!);
  • избегать мусора в начале XML;
  • избегать мусора в конце JSON и XML.
К сожалению, JSON дает только один способ сделать парсинг более строгим: Для просмотра ссылки Войди или Зарегистрируйся. Эта фича запрещает неизвестные поля во входящем JSON. YAML предлагает аналогичную функцию KnownFields(true). А вот для XML была попытка сделать нечто подобное, но Для просмотра ссылки Войди или Зарегистрируйся завернули.

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

  1. Плохая производительность: JSON приходится парсить дважды, что заметно замедляет процесс.
  2. Неполная детекция: в некоторых крайних случаях недочеты все‑таки остаются, как указано в комментариях к функции.
  3. Низкий потенциал внедрения: эти меры безопасности не встроены в библиотеки как защищенные настройки по умолчанию или настраиваемые опции, так что массовое распространение вряд ли получится.
Но если ты вдруг обнаружишь уязвимость в своем коде, то даже такое несовершенное решение может помочь затыкать дыру, пока ты работаешь над более надежным вариантом.

Код:
// DetectCaseInsensitiveKeyCollisions проверяет, есть ли в JSON-данных ключи, которые различаются только регистром букв. Это помогает предотвратить скрытые баги, где два ключа с разным написанием могут ссылаться на одни и те же данные.
func DetectCaseInsensitiveKeyCollisions(data []byte) error {
  // Создаем карту для хранения декодированных JSON-данных и пытаемся распарсить JSON. Это сохраняет ключи с разным регистром.
  var res map[string]interface{}
  if err := json.NewDecoder(bytes.NewReader(data)).Decode(&res); err != nil {
    return err
  }

  seenKeys := make([]string, 0, len(res))

  // Пробежимся по всем ключам в распарсенном JSON и поищем дубликаты
  for newKey := range res {
    for _, existingKey := range seenKeys {
      if strings.EqualFold(existingKey, newKey) {
        // Вернуть ошибку, если найден дубликат без учета регистра
        return fmt.Errorf("найдены дубликаты ключей без учета регистра:
                        %q и %q", existingKey, newKey)
      }
    }
    seenKeys = append(seenKeys, newKey)
  }
  return nil
}

// Обеспечивает более строгий парсинг JSON с дополнительной проверкой:
//   1. Отвергает неизвестные поля, которых нет в целевой структуре
//   2. Выявляет дубликаты ключей без учета регистра
//   3. Проверяет полный парсинг без остаточных данных
// strictJSONParse НЕ делает следующее:
//   - Не гарантирует отсутствие дублирующих ключей с одинаковым регистром
//   - Не проверяет, совпадает ли регистр во входных данных с ожидаемым регистром в целевой структуре
func strictJSONParse(jsonData []byte, target interface{}) error {
  decoder := json.NewDecoder(bytes.NewReader(jsonData))

  // 1. Запретить неизвестные поля
  decoder.DisallowUnknownFields()

  // 2. Запретить дублирующиеся ключи с разным регистром
  err := DetectCaseInsensitiveKeyCollisions(jsonData)
  if err != nil {
    return fmt.Errorf("strictJSONParse: %w", err)
  }

  // Декодируем JSON в переданную структуру
  err = decoder.Decode(target)
  if err != nil {
    return fmt.Errorf("strictJSONParse: %w", err)
  }

  // 3. Убедимся, что нет остаточных данных после JSON-объекта
  token, err := decoder.Token()
  if err != io.EOF {
    return fmt.Errorf("strictJSONParse: неожиданные дополнительные данные после JSON: token: %v, err: %v", token, err)
  }

  return nil
}

JSON 2.0​

Чтобы фича стала массовой и действительно решила проблему на глобальном уровне, ее нужно вшить в библиотеку и включить по умолчанию. Вот тут‑то и вступает в игру Для просмотра ссылки Войди или Зарегистрируйся — новая версия библиотеки парсинга JSON для Go. Пока это только предложение, но огромная работа уже проделана, и мы надеемся, что совсем скоро этот стандарт выйдет. JSON v2 превосходит первую версию по многим параметрам:

  • Запрещены дублирующиеся имена: «...в версии v2 JSON-объект с дублирующимися именами приводит к ошибке. Поведение регулируется опцией jsontext.AllowDuplicateNames».
  • Учитывается регистр при совпадении: «...в v2 поля совпадают точно, с учетом регистра. Опции MatchCaseInsensitiveNames и jsonv1.MatchCaseSensitiveDelimiter контролируют это поведение».
  • Есть опция RejectUnknownMembers, хотя она не включена по умолчанию (аналогична DisallowUnknownFields).
  • Есть возможность обрабатывать данные из io.Reader с помощью функции UnmarshalRead, проверяя наличие EOF и не допуская лишних данных в конце.
Хотя это предложение решает многие обсуждаемые здесь проблемы, трудности в экосистеме Go сохранятся, пока новшество не наберет популярность. Сперва нужно официальное одобрение, после чего разработчикам придется внедрять это решение в весь существующий код на Go, парсящий JSON. Пока этого не случится, уязвимости продолжат создавать риски.


Памятка для разработчика​

  1. Включи строгий парсинг по умолчанию. Для JSON используй DisallowUnknownFields, для YAML — KnownFields(true). Увы, это все, что реально можно сделать с API парсера напрямую в Go.
  2. Сохраняй консистентность на границах. Когда данные проходят через несколько сервисов, убедись, что парсинг работает последовательно, — используй одинаковый парсер или добавь дополнительные уровни валидации, например такую штуку, как strictJSONParse.
  3. Следи за прогрессом JSON v2. Поглядывай за разработкой библиотеки Для просмотра ссылки Войди или Зарегистрируйся для Go, которая решает массу проблем, предлагая более безопасные дефолты.
  4. Используй статический анализ. Врубай правила Semgrep, чтобы выловить уязвимые паттерны в своем коде, особенно некорректное использование тега - и полей omitempty. Попробуй запустить созданные нами правила.

Выводы​

Мы предложили способы смягчить последствия и методы обнаружения, но в долгосрочной перспективе все равно придется менять подход к работе парсеров. Пока библиотеки парсеров не выберут безопасность по умолчанию, разработчикам нужно держать ухо востро.
 
Activity
So far there's no one here