sdp: bound DataElement parse recursion to prevent RecursionError DoS#914
sdp: bound DataElement parse recursion to prevent RecursionError DoS#914ibondarenko1 wants to merge 2 commits intogoogle:mainfrom
Conversation
DataElement.from_bytes -> list_from_bytes -> (SEQUENCE/ALTERNATIVE constructor dispatches back to list_from_bytes) had no depth limit. A malicious SDP peer could send a PDU of a few kilobytes containing ~1000 nested SEQUENCE tags and exhaust the Python recursion stack, crashing the host with an unhandled RecursionError propagating out of the SDP handler. Reachable via: any remote Bluetooth device that Bumble performs SDP service discovery against (default during Classic connection setup). Same family as PR google#912 (ATT_PDU.from_bytes empty PDU IndexError) - remote unchecked-input parser crash in the Bluetooth stack. Fix: thread-local depth counter, cap nesting at 32 (well above anything a legitimate service record uses). Added two regression tests covering the deep-nesting reject path and normal 16-level-nested SEQUENCE parsing. Reproducer (4.5 KB payload, deterministic crash on 0.0.228): from bumble.sdp import DataElement inner = b"\x35\x00" for _ in range(1500): size = len(inner) if size < 65535: inner = bytes([0x36, (size >> 8) & 0xFF, size & 0xFF]) + inner DataElement.from_bytes(inner) # RecursionError before fix Signed-off-by: ibondarenko1 <ibondarenko1@users.noreply.github.com>
|
I personally don't suggest that we need much protection agaist attacks here, as Bumble is not designed to be a product providing safety, but a tooling stack for test and study, so additional protection might limit the readability and flexibility for our major purpose. Maybe need opinion from @barbibulle |
|
Reasonable point, but there's a direct precedent — the ATT_PDU IndexError fix in #912 (same DoS class, same "malformed remote peer PDU crashes parser") was merged recently. That finding was also surfaced by fuzzing and the fix shipped in production Bumble. If #912 was in-scope as "hardening against malformed peer input," #914 applies the same reasoning to SDP's nested SEQUENCE parser — a 4.5KB PDU exhausts the Python stack. The depth cap is 32 (>10× realistic SDP records), so the legitimate flexibility for testing/research is unaffected. Happy to defer to @barbibulle's read; just wanted to name the precedent. |
|
#912 is also debatable - if we decide fuzzing is not applicable to Bumble, we might revert it. In many previous PRs, we actually minimized the validation of packets or parameters, so that users can send something invalid to controllers or remote devices to test edge cases. If we are making products for innocent users like Android or BlueZ, that's definitely necessary to protect the stack, but all users of Bumble are (should be?) developers responsible for themselves. |
|
Totally fair framing on target audience — developers responsible for their own robustness, I get the philosophy. The specific angle I had in mind was narrower: CI / fuzz-rig hygiene. Bumble is used downstream in automated discovery against arbitrary remote devices (Hermes fuzzer, interop lab rigs, BLE research pipelines). One malicious peer nearby → If that narrower reading isn't compelling either, happy to close the PR — no strong feelings. Just wanted to put that use case on the table before @barbibulle weighs in. |
|
I agree with @zxzxwu 's general statement that trying to make the stack robust against attacks would distract from its main goal. And it would take a lot of effort to actually do that, since there are so many code paths that process external data. |
|
Thanks for the review @barbibulle — appreciate the framing, happy to land this strictly as a tooling-ergonomics fix. Pushed CI runs are currently queued as |
| break | ||
| inner = bytes([0x36, (size >> 8) & 0xFF, size & 0xFF]) + inner | ||
|
|
||
| import pytest |
| # prevent a malicious peer from crashing the process via a deeply nested PDU. | ||
| # 32 levels is well beyond anything a legitimate service record uses. | ||
| _MAX_DATA_ELEMENT_NESTING = 32 | ||
| _parse_state = threading.local() |
There was a problem hiding this comment.
Not prefer to use a global threading local, but we can leave it here and refactor later.
| return elements | ||
| depth = getattr(_parse_state, "depth", 0) | ||
| if depth >= _MAX_DATA_ELEMENT_NESTING: | ||
| raise ValueError( |
DataElement.from_bytes -> list_from_bytes -> (SEQUENCE/ALTERNATIVE constructor dispatches back to list_from_bytes) had no depth limit. A malicious SDP peer could send a PDU of a few kilobytes containing ~1000 nested SEQUENCE tags and exhaust the Python recursion stack, crashing the host with an unhandled RecursionError propagating out of the SDP handler.
Reachable via: any remote Bluetooth device that Bumble performs SDP service discovery against (default during Classic connection setup).
Same family as PR #912 (ATT_PDU.from_bytes empty PDU IndexError) - remote unchecked-input parser crash in the Bluetooth stack.
Fix: thread-local depth counter, cap nesting at 32 (well above anything a legitimate service record uses). Added two regression tests covering the deep-nesting reject path and normal 16-level-nested SEQUENCE parsing.
Reproducer (4.5 KB payload, deterministic crash on 0.0.228):