Содержание статьи
- Парсинг в Go
- Атака 1: подмена данных при (де)сериализации
- Поля без меток
- Неправильное использование тега -
- Ошибки с omitempty
- Атака 2: разные парсеры — разные результаты
- Дубли полей
- Поиск по ключам без учета регистра
- Атака 3: путаница в форматах данных
- Неизвестные ключи
- Мусорные данные в начале
- Мусорные данные в конце
- Создаем полиглот
- Способы защиты
- JSON 2.0
- Памятка для разработчика
- Выводы
Это близкий к тексту пересказ статьи «Unexpected security footguns in Go’s parsers» из блога Trail of Bits. Ее автор — Васко Франко. Материал доступен без платной подписки.
Это не просто теоретические баги — они уже привели к реальным уязвимостям, таким как CVE-2020-16250 (обход аутентификации в HashiCorp Vault, который обнаружили ребята из Google Project Zero), и множеству критических находок в проектах наших клиентов.
Здесь мы расскажем об этих неожиданных глюках парсеров и приведем три сценария атаки, которые должен знать каждый инженер по безопасности и разработчик на Go.
- (Де)сериализация неожиданных данных: как парсеры на Go могут раскрыть данные, которые разработчики планировали оставить приватными.
- Различия парсеров: как несовпадения в работе разных парсеров позволяют хакерам обходить меры безопасности, когда несколько сервисов обрабатывают одинаковые данные.
- Путаница в форматах данных: как парсеры обрабатывают межформатные данные с неожиданными и порой весьма взрывоопасными результатами.
Мы покажем каждый сценарий атаки на примере из реальной жизни и завершим все конкретными рекомендациями о том, как безопаснее использовать парсинг. В том числе расскажем, как затыкать дыры в безопасности стандартной библиотеки Go.
Вот краткий обзор неожиданных поведений, которые мы рассмотрим, с индикаторами.
Особенность | JSON | JSON v2 | XML | YAML |
---|---|---|---|---|
json:"-," | Да (плохой дизайн) |
Да (плохой дизайн) |
Да (плохой дизайн) |
Да (плохой дизайн) |
json:"omitempty" | Да (как и ожидалось) |
Да (как и ожидалось) |
Да (как и ожидалось) |
Да (как и ожидалось) |
Дублирующиеся ключи | Да (последний) |
Нет | Да (последний) |
Нет |
Регистронезависимость | Да | Нет | Нет | Нет |
Неизвестные ключи | Да (исправимо) |
Да (исправимо) |
Да | Да (исправимо) |
Лишние ведущие данные | Нет | Нет | Да | Нет |
Лишние данные в конце | Да (с Decoder) |
Нет | Да | Нет |
Парсинг в Go
Давай разберем, как Go обрабатывает JSON, XML и YAML. В стандартной библиотеке Go найдутся парсеры для JSON и XML, а вот для YAML придется подобрать что‑то стороннее — на выбор есть куча реализаций. В этой статье мы сосредоточимся на таких парсерах:
- encoding/json, версия go1.24.1;
- encoding/xml, версия go1.24.1;
- yaml.v3, версия 3.0.1, самая популярная сторонняя библиотека YAML для Go.
В наших примерах будем использовать 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.
вместо byte-срезов. Такой API особенно хорош для парсинга потоковых данных, например тел HTTP-запросов, и поэтому часто применяется в системах обработки HTTP.

Атака 1: подмена данных при (де)сериализации
Иногда нужно ограничить, какие именно поля структуры могут быть сериализованы или десериализованы.
Давай разберем простой пример: есть бэкенд‑сервер, на котором настроен HTTP-хендлер для создания пользователей и еще один — для их извлечения после аутентификации.
Когда создаешь пользователя, возможно, тебе не захочется, чтобы он мог задать поле IsAdmin
сам (то есть парсить это поле из пользовательского ввода).

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

Как можно указать парсерам не сериализовать или десериализовать поле?
Поля без меток
Давай сначала посмотрим, что произойдет, если ты не установишь тег 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-правило. Оно не попало в наш каталог правил на Semgrep registry, так как в зависимости от твоего кода велика вероятность получить кучу ложных срабатываний.
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
. Мы по ошибке скопировали часть ,
, из‑за чего парсер начал искать ключ -
в предоставленном JSON. Я пробежался по топ-1000 репозиториев на Go на GitHub с наибольшим количеством звезд и среди прочих нашел два таких случая (и отрапортовал о них, так что их уже исправили):
- в Flipt поле
ClientID
в конфигурации OIDC отображалось как-
(исправлено в #3658); - в langchaingo поле
MaxTokens
также отображалось как-
(исправлено в #1163).
Это поведение чревато ошибками и не приносит особой пользы (ну кроме возможности назвать поле -
), и тем не менее оно описано в документации пакета JSON.
info
Особый случай: если у тега поля стоит -
, то это поле всегда пропускается. Фокус в том, что поле с именем -
все еще можно создать, используя тег -,
.
Парсеры для XML и YAML работают похоже, но есть один подвох: XML-парсер считает тег <
некорректным. Чтобы это исправить, нужно добавить для символа «минус» пространство имен. Например, превратить его в <
.

Хорошо, давай на этот раз все сделаем правильно.
type User struct { // Имя пользователя в JSON, если не пустое Username string `json:"username,omitempty"` // Пароль в JSON, если не пустой Password string `json:"password,omitempty"` // Не отображать признак админа в JSON IsAdmin bool `json:"-"`}
Наконец‑то! Теперь нет никакой возможности для десериализации поля IsAdmin
.
Ты, наверное, спросишь: как же эти неправильные настройки могут превратиться в уязвимости в безопасности? Самый банальный способ — это, как в нашем примере, использовать -,..
как JSON-тег для поля типа IsAdmin
, которое пользователь ни в коем случае не должен контролировать.
Детектить такую штуку обычными юнит‑тестами очень сложно, ведь без специального теста, который распаковывает данные с ключом -
и проверяет, изменилось ли поле, ты такой баг не поймаешь. Тут нужны либо какие‑то продвинутые фичи IDE, либо сторонний инструмент.

Мы разработали публичное правило для Semgrep, которое поможет тебе выявить подобные проблемы в коде. Зацени его в деле:
semgrep -c r/trailofbits.go.unmarshal-tag-is-dash
Ошибки с omitempty
Еще одна простая, но веселая ошибка конфигурации, с которой мы сталкивались раньше: разработчик по ошибке задал имя поля как omitempty
.
// Результат: User{Username:"a_user"}
Если ты установишь для JSON-тега omitempty
, то парсер будет использовать omitempty
как имя поля (что ожидаемо). Некоторые разработчики пытались так хитрить: ставить omitempty
в качестве опции и при этом оставлять стандартное имя поля. Я пошерстил топ-1000 репозиториев на Go в поисках такого трюка, и вот что удалось нарыть:
- Gitea палился, выставляя наружу поле
Args
в структуреTranslatableMessage
с припискойomitempty
(залатано в пул‑реквесте #33663). - Подобная история у Kustomize: у него засветилось поле
Replacements
в структуреplugin
, тоже сomitempty
(поправлено в #5877). - Btcd тоже отличился, выдав поле
MaxFeeRate
в структуреTestMempoolAcceptCmd
с тем самымomitempty
. - Evcc последовал той же дорожкой, показав поле
Message
в структуреMeasurements
с ключомomitempty
.
Как видишь, разработчики часто хотят установить тег в значение json:
, чтобы сохранить имя по умолчанию и добавить опцию omitempty
.
В отличие от предыдущего примера, этот вряд ли повлияет на безопасность и должен легко выявляться в тестах. Любая попытка сериализовать или десериализовать ввод с ожидаемым именем поля провалится. Однако, как ни странно, такое все еще встречается даже в популярных опенсорсных репозиториях. Мы сделали публичное правило для Semgrep, чтобы помочь тебе находить подобные баги в твоих проектах. Использовать так:
semgrep -c r/trailofbits.go.unmarshal-tag-is-omitempty
Атака 2: разные парсеры — разные результаты
Что произойдет, если разобрать одни и те же данные с помощью разных JSON-парсеров и они выдадут разные результаты? И самое интересное — какие особенности парсеров в Go дают злоумышленникам возможность стабильно провоцировать такие расхождения?
Возьмем для примера приложение, построенное на микросервисной архитектуре. В его составе:
- прокси‑сервис, который обрабатывает все запросы пользователей;
- сервис авторизации, на который прокси‑сервис ссылается, чтобы проверить, есть ли у пользователя нужные права для выполнения его запроса;
- набор сервисов бизнес‑логики, которые прокси‑сервис дергает, чтобы реализовать бизнес‑логику.
В этом сценарии обычный пользователь без прав админа пытается выполнить UserAction
— действие, которое ему разрешено выполнять.

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

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

Сервис авторизации, который написан на другом языке программирования или использует нестандартный парсер для Go, будет парсить UserAction
и даст пользователю права на выполнение операции. А вот прокси‑сервис, который использует стандартный парсер Go, разберет AdminAction
и отправит его не тому сервису. Остается вопрос: какие нагрузки мы можем создать, чтобы добиться такого поведения?
Это довольно популярная архитектура, которую нам доводилось встречать во время наших аудитов, и именно в ней мы обнаруживали обход аутентификации. Проблемы, о которых мы расскажем ниже, делают это возможным. Есть и другие примеры, но большинство из них следуют той же модели: компонент, отвечающий за проверку безопасности, и компонент, осуществляющий действия, по‑разному видят входные данные. Вот несколько таких примеров в различных сценариях:
- CVE-2017-12635: уязвимость обхода авторизации в Apache CouchDB из‑за различий JSON-парсеров (очень похоже на наш пример);
- побег из песочницы macOS из‑за различий XML-парсеров (2020);
- удаленное выполнение кода в Zoom без взаимодействия пользователя из‑за различий XML-парсеров в XMPP (2022, PDF);
- обход аутентификации SAML в GitLab из‑за различий XML-парсеров (2025).
Дубли полей
Первая уязвимость, которую мы разберем, — это дублирование ключей. Что будет, если во входном 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"}
Это противоречит документации, где сказано:
Чтобы распарсить JSON в структуру, метод
Unmarshal
сопоставляет ключи входящего объекта с ключами, которые используетMarshal
(либо имя поля структуры, либо его тег), предпочитая точное совпадение, но также принимая совпадение без учета регистра.
Ты можешь даже использовать символы 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 в разных парсерах, рекомендуем прочитать эти два поста:
- Parsing JSON is a Minefield (Николя Серио);
- JSON Interoperability Vulnerabilities (Бишоп Фокс).
Атака 3: путаница в форматах данных
Для финальной атаки давай посмотрим, что будет, если распарсить JSON-файл XML-парсером или использовать какой‑нибудь другой неподходящий формат.
Возьмем для примера CVE-2020-16250: байпас защиты HashiCorp Vault в методе аутентификации через AWS IAM. Эту уязвимость обнаружила команда Google Project Zero (если интересно погрузиться в детали, в блоге есть пост под названием Enter the Vault: Authentication Issues in HashiCorp Vault). Мы не будем вываливать здесь все тонкости, но в целом вот как выглядит стандартный процесс аутентификации HashiCorp Vault через AWS IAM:
- AWS-ресурс, скажем функция AWS Lambda, подписывает запрос GetCallerIdentity.
- Этот запрос отправляется на сервер Vault.
- Сервер Vault собирает запрос и пересылает его в AWS Security Token Service (STS).
- AWS STS проверяет подпись.
- Если все окей, AWS STS возвращает XML-документ с данными о роли.
- Сервер Vault парсит XML, извлекает идентификатор и, если у этой роли есть доступ к запрашиваемым секретам, отправляет их обратно.
- Теперь AWS-ресурс может использовать секреты, например чтобы авторизоваться в базе данных.

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

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

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

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

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

Создаем полиглот
Как объединить все рассмотренные нами способы поведения, чтобы создать файл‑полиглот, который:
- можно обработать парсерами 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 дает только один способ сделать парсинг более строгим: DisallowUnknownFields. Эта фича запрещает неизвестные поля во входящем JSON. YAML предлагает аналогичную функцию KnownFields(
. А вот для XML была попытка сделать нечто подобное, но предложение завернули.
Чтобы покончить с оставшимися косяками стандартных настроек безопасности, нам придется сочинить что‑то свое, кастомное и немного хакерское. Взгляни на следующий блок кода с функцией strictJSONParse
. Это наша попытка сделать разбор JSON строже, хотя тут есть свои ограничения:
- Плохая производительность: JSON приходится парсить дважды, что заметно замедляет процесс.
- Неполная детекция: в некоторых крайних случаях недочеты все‑таки остаются, как указано в комментариях к функции.
- Низкий потенциал внедрения: эти меры безопасности не встроены в библиотеки как защищенные настройки по умолчанию или настраиваемые опции, так что массовое распространение вряд ли получится.
Но если ты вдруг обнаружишь уязвимость в своем коде, то даже такое несовершенное решение может помочь затыкать дыру, пока ты работаешь над более надежным вариантом.
// 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 v2 — новая версия библиотеки парсинга JSON для Go. Пока это только предложение, но огромная работа уже проделана, и мы надеемся, что совсем скоро этот стандарт выйдет. JSON v2 превосходит первую версию по многим параметрам:
- Запрещены дублирующиеся имена: «...в версии v2 JSON-объект с дублирующимися именами приводит к ошибке. Поведение регулируется опцией
jsontext.
».AllowDuplicateNames - Учитывается регистр при совпадении: «...в v2 поля совпадают точно, с учетом регистра. Опции
MatchCaseInsensitiveNames
иjsonv1.
контролируют это поведение».MatchCaseSensitiveDelimiter - Есть опция
RejectUnknownMembers
, хотя она не включена по умолчанию (аналогичнаDisallowUnknownFields
). - Есть возможность обрабатывать данные из
io.
с помощью функцииReader UnmarshalRead
, проверяя наличие EOF и не допуская лишних данных в конце.
Хотя это предложение решает многие обсуждаемые здесь проблемы, трудности в экосистеме Go сохранятся, пока новшество не наберет популярность. Сперва нужно официальное одобрение, после чего разработчикам придется внедрять это решение в весь существующий код на Go, парсящий JSON. Пока этого не случится, уязвимости продолжат создавать риски.
Памятка для разработчика
-
Включи строгий парсинг по умолчанию. Для JSON используй
DisallowUnknownFields
, для YAML —KnownFields(
. Увы, это все, что реально можно сделать с API парсера напрямую в Go.true) -
Сохраняй консистентность на границах. Когда данные проходят через несколько сервисов, убедись, что парсинг работает последовательно, — используй одинаковый парсер или добавь дополнительные уровни валидации, например такую штуку, как
strictJSONParse
. - Следи за прогрессом JSON v2. Поглядывай за разработкой библиотеки JSON v2 для Go, которая решает массу проблем, предлагая более безопасные дефолты.
-
Используй статический анализ. Врубай правила Semgrep, чтобы выловить уязвимые паттерны в своем коде, особенно некорректное использование тега
-
и полейomitempty
. Попробуй запустить созданные нами правила.
Выводы
Мы предложили способы смягчить последствия и методы обнаружения, но в долгосрочной перспективе все равно придется менять подход к работе парсеров. Пока библиотеки парсеров не выберут безопасность по умолчанию, разработчикам нужно держать ухо востро.