Skip to content

[Security Advisory] ReDoS in JSONata $toMillis via duplicate f-component picture string (bypass of CVE-2026-52746) #833

Description

@1diot9

attachment.zip

Image

Description

Summary

JSONata is a JSON query and transformation language implementation for JavaScript (npm package jsonata, v2.2.2, MIT license). The built-in function $toMillis(timestamp, picture) converts a date string to milliseconds since epoch. When a picture string is provided, the function delegates to parseDateTime, which dynamically generates a regular expression from the picture string and matches the input against it. The f (fractional seconds) component in generateRegex unconditionally emits [0-9]+ regardless of the parseWidth defense mechanism, allowing an attacker to craft a picture string with multiple [f1] markers that produces a regex with multiple adjacent [0-9]+ capturing groups, causing polynomial backtracking (O(n^(k-1))) on non-matching inputs. This bypasses CVE-2026-52746, whose fix (PR #793) only patched the hardcoded iso8601regex used when no picture string is provided.

Details

Root cause: In src/src/datetime.js, the generateRegex function (line 954) processes each component of the picture string. The f component branch (line 997-998) is checked before the integerFormat branch (line 1002) and always sets res.regex = '[0-9]+', completely ignoring the parseWidth property. The parseWidth defense mechanism (line 628-632) sets previousPart.integerFormat.parseWidth = previousPart.integerFormat.mandatoryDigits when processing adjacent integer components, but since the f component does not go through the integerFormat branch, parseWidth has no effect on it.

Attack path: An attacker provides a JSONata expression $toMillis(malicious_input, '[f1][f1][f1][f1][f1][f1][f1]'). The analyseDateTimePicture function parses the picture string into 7 f component markers. The generateRegex function produces 7 parts each with regex = '[0-9]+'. The parseDateTime function constructs the full regex ^([0-9]+)([0-9]+)([0-9]+)([0-9]+)([0-9]+)([0-9]+)([0-9]+)$ and executes it via new RegExp(fullRegex, 'i').exec(timestamp) (line 1141-1142). For a non-matching input of 80 digits followed by 'x', the regex engine tries all possible ways to split 80 digits among 7 groups, resulting in O(n^6) polynomial backtracking. Harness testing confirms the polynomial growth: 30 digits = 22ms, 50 digits = 163ms, 60 digits = 713ms, 70 digits = 1.8s, 80 digits = ~25s, 100 digits = ~23s.

Additional exposure: The parseDateTime function uses new RegExp() directly (line 1141) instead of environment.base.RegexEngine, so the RegexEngine option (which allows callers to plug in a ReDoS-safe regex engine) does not protect this code path. The timeout guardrail also does not cover native RegExp execution (documented in guardrails.md). In the default configuration, there is no protection against this ReDoS.

PoC

# API call recipe (component library — no HTTP endpoint)
const jsonata = require('jsonata');
const expr = jsonata('$toMillis("<DIGIT_STRING>x", "[f1][f1][f1][f1][f1][f1][f1]")');
await expr.evaluate({});
# Where <DIGIT_STRING> is 80+ '0' characters
# Expected: evaluation hangs for ~25 seconds (80-digit input)

Impact

An attacker who can control the JSONata expression string (e.g., in an online JSONata playground, rule engine, or data transformation pipeline that accepts user-supplied expressions) can cause the host application to hang for tens of seconds or longer by evaluating $toMillis with a crafted picture string and malicious input. This results in denial of service. The vulnerability exists in the default configuration with no guardrails effective against it.

Affected products

  • jsonata >= 2.2.0 (including the CVE-2026-52746 fix) — confirmed on 2.2.2
  • jsonata >= 2.0.0, < 2.2.0 — original CVE path also vulnerable, this path additionally exploitable
  • jsonata 1.x branch — requires verification

Severity / CWE

CWE: CWE-400 (Uncontrolled Resource Consumption)

CVSS 3.1: 7.5 (High)

  • Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions