The integer fallback in generated RootBuilder code strips leading zeros one byte at a time from the front of a std::string. Each erase(digits.begin()) is O(n), so a number with k leading zeros after the decimal point takes O(k^2) time to parse. This makes services using the generated parser vulnerable to algorithmic-complexity denial of service.
Relevant code
File: contrib/schemagen/weaseljson_schemagen.py
Line: 999 (inside the generated parseJsonInt64 function template)
Generate a parser and feed it a number of the form 0.<N-1 zeros>1eN. This is valid JSON with value 1, so it is accepted, but the parser spends quadratic time in the loop above.
Measured on a local build:
N
input length
time
50k
~50 KB
36 ms
100k
~100 KB
115 ms
200k
~200 KB
486 ms
400k
~400 KB
1975 ms
Root cause
parseJsonInt64 builds a temporary std::string digits containing the integer part followed by all fractional digits, strips trailing zeros, and then removes leading zeros with repeated erase(digits.begin()). Because erasing from the front of a std::string shifts every remaining character, k leading zeros require roughly 1 + 2 + ... + k = O(k^2) byte moves.
The JSON grammar permits leading zeros after the decimal point, so inputs like 0.000...0001eN are valid and accepted by the underlying WeaselJson parser. The generated std::from_chars path rejects them because they contain . and e, so the fallback parseJsonInt64 is always used for this shape.
Expected behavior
Parsing time should scale linearly with the length of the number (or at least not quadratically).
Actual behavior
Parsing time scales quadratically with the number of leading fractional zeros. A few hundred kilobytes of JSON can keep a thread busy for seconds; a few megabytes can keep it busy for minutes.
Impact
Any generated parser with an integer slot is vulnerable to a remote DoS if an attacker can supply a JSON number. The attack payload is small (hundreds of kilobytes), does not require nesting depth, and avoids the parser's explicit depth/overflow limits.
The integer fallback in generated `RootBuilder` code strips leading zeros one byte at a time from the front of a `std::string`. Each `erase(digits.begin())` is O(n), so a number with k leading zeros after the decimal point takes O(k^2) time to parse. This makes services using the generated parser vulnerable to algorithmic-complexity denial of service.
**Relevant code**
File: `contrib/schemagen/weaseljson_schemagen.py`
Line: 999 (inside the generated `parseJsonInt64` function template)
```cpp
while (!digits.empty() && digits.front() == '0') digits.erase(digits.begin());
```
**Reproduction**
Schema (`int_schema.json`):
```json
{"type": "integer"}
```
Generate a parser and feed it a number of the form `0.<N-1 zeros>1eN`. This is valid JSON with value `1`, so it is accepted, but the parser spends quadratic time in the loop above.
Measured on a local build:
| N | input length | time |
|-------|--------------|--------|
| 50k | ~50 KB | 36 ms |
| 100k | ~100 KB | 115 ms |
| 200k | ~200 KB | 486 ms |
| 400k | ~400 KB | 1975 ms| (roughly 4x per doubling)
**Root cause**
`parseJsonInt64` builds a temporary `std::string digits` containing the integer part followed by all fractional digits, strips trailing zeros, and then removes leading zeros with repeated `erase(digits.begin())`. Because erasing from the front of a `std::string` shifts every remaining character, k leading zeros require roughly 1 + 2 + ... + k = O(k^2) byte moves.
The JSON grammar permits leading zeros after the decimal point, so inputs like `0.000...0001eN` are valid and accepted by the underlying `WeaselJson` parser. The generated `std::from_chars` path rejects them because they contain `.` and `e`, so the fallback `parseJsonInt64` is always used for this shape.
**Expected behavior**
Parsing time should scale linearly with the length of the number (or at least not quadratically).
**Actual behavior**
Parsing time scales quadratically with the number of leading fractional zeros. A few hundred kilobytes of JSON can keep a thread busy for seconds; a few megabytes can keep it busy for minutes.
**Impact**
Any generated parser with an `integer` slot is vulnerable to a remote DoS if an attacker can supply a JSON number. The attack payload is small (hundreds of kilobytes), does not require nesting depth, and avoids the parser's explicit depth/overflow limits.
weaselbot
was assigned by andrew2026-06-30 15:45:42 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The integer fallback in generated
RootBuildercode strips leading zeros one byte at a time from the front of astd::string. Eacherase(digits.begin())is O(n), so a number with k leading zeros after the decimal point takes O(k^2) time to parse. This makes services using the generated parser vulnerable to algorithmic-complexity denial of service.Relevant code
File:
contrib/schemagen/weaseljson_schemagen.pyLine: 999 (inside the generated
parseJsonInt64function template)Reproduction
Schema (
int_schema.json):Generate a parser and feed it a number of the form
0.<N-1 zeros>1eN. This is valid JSON with value1, so it is accepted, but the parser spends quadratic time in the loop above.Measured on a local build:
Root cause
parseJsonInt64builds a temporarystd::string digitscontaining the integer part followed by all fractional digits, strips trailing zeros, and then removes leading zeros with repeatederase(digits.begin()). Because erasing from the front of astd::stringshifts every remaining character, k leading zeros require roughly 1 + 2 + ... + k = O(k^2) byte moves.The JSON grammar permits leading zeros after the decimal point, so inputs like
0.000...0001eNare valid and accepted by the underlyingWeaselJsonparser. The generatedstd::from_charspath rejects them because they contain.ande, so the fallbackparseJsonInt64is always used for this shape.Expected behavior
Parsing time should scale linearly with the length of the number (or at least not quadratically).
Actual behavior
Parsing time scales quadratically with the number of leading fractional zeros. A few hundred kilobytes of JSON can keep a thread busy for seconds; a few megabytes can keep it busy for minutes.
Impact
Any generated parser with an
integerslot is vulnerable to a remote DoS if an attacker can supply a JSON number. The attack payload is small (hundreds of kilobytes), does not require nesting depth, and avoids the parser's explicit depth/overflow limits.