Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
Message-ID: <CADCSfHUqviT_t31RU18UJf6ENcS3kLP4Y6kmj_W=k9Oda5osvw@mail.gmail.com>
Date: Mon, 14 Sep 2026 21:00:05 +0300
From: Evgenios Gkritsis <evgeniosgkritsis@...il.com>
To: oss-security@...ts.openwall.com
Subject: rosbridge_library Protocol.incoming() quadratic CPU cost in JSON fallback

Hello,

I am reporting an algorithmic-complexity defect in
rosbridge_library.protocol.Protocol.incoming() in rosbridge_suite. It is
present in the current code on the ros2 branch and was not removed by PR
#1199.

Affected:

Product:  rosbridge_suite (rosbridge_library)
Component: Protocol.incoming() JSON fallback
Versions: current ros2 branch; present since the fallback was introduced
CWE:  CWE-407 (Inefficient Algorithmic Complexity) / CWE-1050


Vulnerable code:

When json.loads(buffer) fails, incoming() falls back to scanning the buffer
for every { and } pair:

```python
#!/usr/bin/env python3

import json
import math
import statistics
import time


def vulnerable_fallback(buffer: str):
    """
    Reproduce the current rosbridge Protocol.incoming() fallback:

        opening_brackets = [...]
        closing_brackets = [...]

        for start in opening_brackets:
            for end in closing_brackets:
                try:
                    json.loads(buffer[start:end + 1])
                except Exception:
                    pass

    We deliberately provide JSON which cannot produce a valid message,
    so the nested loops have to exhaust their candidates.
    """

    opening_brackets = [
        i for i, letter in enumerate(buffer)
        if letter == "{"
    ]

    closing_brackets = [
        i for i, letter in enumerate(buffer)
        if letter == "}"
    ]

    attempts = 0

    for start in opening_brackets:
        for end in closing_brackets:
            attempts += 1

            try:
                msg = json.loads(buffer[start:end + 1])

                if isinstance(msg, dict) and msg.get("op") is not None:
                    return attempts

            except Exception:
                pass

    return attempts


def build_input(n: int) -> str:
    """
    Prevent any candidate substring from being valid JSON.

    Every opening brace is followed by a syntactically incomplete
    JSON object fragment, so the fallback keeps trying candidates.
    """

    return '{"x":' * n + '}' * n


def benchmark(n: int, repeats: int = 3):
    data = build_input(n)

    times = []
    attempts = None

    for _ in range(repeats):
        t0 = time.perf_counter()

        result = vulnerable_fallback(data)

        elapsed = time.perf_counter() - t0

        times.append(elapsed)
        attempts = result

    return (
        len(data),
        attempts,
        statistics.median(times),
    )


def main():
    print("=== rosbridge JSON fallback complexity PoC ===")
    print()

    sizes = [
        50,
        100,
        200,
        400,
        800,
        1200,
        1600,
        2400,
    ]

    results = []

    print(
        f"{'n':>8} "
        f"{'bytes':>10} "
        f"{'attempts':>14} "
        f"{'median(s)':>12}"
    )
    print("-" * 52)

    for n in sizes:
        size, attempts, elapsed = benchmark(n)

        print(
            f"{n:>8} "
            f"{size:>10} "
            f"{attempts:>14,} "
            f"{elapsed:>12.6f}"
        )

        results.append((n, elapsed))

    print()
    print("Local scaling exponent:")

    for (n1, t1), (n2, t2) in zip(
        results,
        results[1:],
    ):
        if t1 > 0 and t2 > 0:
            k = math.log(t2 / t1) / math.log(n2 / n1)
            print(
                f"{n1:>5} -> {n2:<5}: k={k:.3f}"
            )


if __name__ == "__main__":
    main()
```
For a buffer with n opening braces and n closing braces, this performs n²
calls to json.loads().
Each call also slices buffer[start:end + 1], which is a full string copy
--> so the aggregate cost is worse than quadratic.

PoC and measurements:

I measured the fallback in isolation with a buffer of n balanced brace
pairs that contains no valid JSON substring, so every inner json.loads()
call fails and the full loop runs:

text
       n      bytes       attempts    median(s)
----------------------------------------------------
      50        300          2,500     0.012958
     100        600         10,000     0.047005
     200       1200         40,000     0.284308
     400       2400        160,000     0.974249
     800       4800        640,000     7.461380
    1200       7200      1,440,000    25.122452
    1600       9600      2,560,000    58.779579
    2400      14400      5,760,000   200.800991

Local scaling exponent (largest intervals):
   800 -> 1200 : k=2.994
  1200 -> 1600 : k=2.955
  1600 -> 2400 : k=3.030
The exponent of ~3.0 is consistent with the nested loop plus per-iteration
slice: O(n²) iterations, each costing O(n) for the slice copy.

Relationship to PR #1199:

PR #1199 by tomqext fixed the binary-WebSocket path that made this fallback
run on every message. The PR description states the fallback "is still
there for TCP fragmentation, it just stops being the hot path." The O(n²)
loop was not removed; it was made harder to reach accidentally. This report
concerns the loop itself, not the binary-frame bug that PR #1199 addressed.

Reachability (what I have and have not verified):

The PoC drives the fallback function directly. I have not completed an
end-to-end test through the WebSocket or TCP transport, so I cannot state
the exact frame size an unauthenticated client would need to send. However,
the fallback exists specifically to handle partial or concatenated messages
arriving over TCP, so reaching it should not require malformed input; a
legitimate client sending a fragmented message can trigger it.


I am reporting now because the code path is present and measurable, and the
fix is independent of the reachability question.


Credit:
Evgenios Gkritsis, Athena Research Center & University of Piraeus, Athens,
Greece
Constantinos Patsakis, Athena Research Center & University of Piraeus,
Athens, Greece
George C. Stergiopoulos, Athens University of Economics and Business,
Athens, Greece

I plan to reference this in a research paper on algorithmic-complexity
vulnerabilities in protocol parsers. I will not disclose publicly until a
fix is published or 90 days have passed.

Thank you.

Powered by blists - more mailing lists

Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.