nlohmann / nlohmann/json

Stack overflow in diff(), merge_patch(), and flatten() due to unbounded recursion

Open
#5,393 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
50.6k
Forks
7.5k
Avg merge
4d 17h
Merged PRs (30d)
58

Description

Summary

Three value algorithms recurse once per nesting level with no depth limit, so each crashes the process on a deeply nested value: json::diff(), basic_json::merge_patch() and json_pointer::flatten() (reached through basic_json::flatten()).

The text parser is iterative, so json::parse accepts input of any depth. Any of these three called on such a value is a crash, which matters wherever the value came from somewhere untrusted — JSON Patch and JSON Merge Patch are typically applied to exactly that.

unflatten() is not affected: it iterates the object and lets get_and_create walk each pointer, and it handles a 500,000-level pointer without trouble. I checked, so that nobody has to.

Reproduction

Against develop (734fd305a), -O2, macOS/arm64 with the default 8 MB stack. All three crash at depth 100,000:

#include <nlohmann/json.hpp>
#include <string>

int main()
{
    const std::size_t depth = 100000;
    const auto deep = [&](const char* leaf)
    {
        return nlohmann::json::parse(std::string(depth, '[') + leaf + std::string(depth, ']'));
    };

    const nlohmann::json a = deep("0");
    const nlohmann::json b = deep("1");

    const auto patch = nlohmann::json::diff(a, b);   // SIGSEGV
    return static_cast<int>(patch.size());
}
call result
json::diff(a, b) SIGSEGV
target.merge_patch(patch) SIGSEGV
j.flatten() SIGSEGV
j.unflatten() fine — iterative

merge_patch is measured with nested objects, since it only descends into objects.

Root cause

  • diffbasic_json::diff() (json.hpp:5089) calls itself per element: json.hpp:5120 (arrays), :5168 (objects)
  • merge_patchbasic_json::merge_patch() (json.hpp:5231) calls itself per member: json.hpp:5247
  • flattenjson_pointer::flatten() (json_pointer.hpp:861) calls itself per element: json_pointer.hpp:879 (arrays), :898 (objects)

Suggested fix

The same shape used for the destructor in #1436 and for copying, serializing and comparing in #5389 / #5285 / #5390: descend a bounded number of levels, then finish the rest on an explicit stack.

flatten looks the most straightforward of the three, since it only accumulates into result and carries a prefix string down. diff returns a value built from each level's result, and merge_patch mutates in place, so both need a little more care about what the explicit stack has to carry.

Related

  • #5387 — the same problem in the copy constructor and dump()
  • #5104 — the same problem in the binary readers
  • #1436 — where the destructor was fixed this way

Written by Claude Code.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the 100,000-level reproducer against develop, then inspect basic_json::diff() and basic_json::merge_patch() in json.hpp alongside json_pointer::flatten() in json_pointer.hpp. Compare their recursive call sites with the iterative approach referenced in #1436 and #5389. Done means all three operations handle deeply nested values without stack overflow while preserving their existing results.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend-api-design, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.