fix(security): use constant-time comparison for BlueBubbles webhook auth - #44025
fix(security): use constant-time comparison for BlueBubbles webhook auth#44025zapabob wants to merge 1 commit into
Conversation
The inbound webhook handler validated the shared password with a plain != comparison, which short-circuits on the first differing byte and leaks the matched prefix length through response timing (CWE-208). Switch to hmac.compare_digest and coerce a missing credential to an empty string so unauthenticated requests are still rejected without raising.
|
Verification: LGTM ✅ Reviewed the diff and tests. The webhook auth check correctly handles the Checked:
Clean implementation, no issues found. |
|
Verified: constant-time webhook auth is correctly implemented. Reviewed the full diff — the fix replaces plain Note: this is part of a cluster with #44026 (Slack SSRF guard) and #44027 (Google Meet constant-time token) — all three follow the same CWE-208 pattern and are independently clean. |
|
Duplicate of #9219 — same |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused security hardening. The current-main premise is valid: gateway/platforms/bluebubbles.py:883 still uses token != self.password, and the proposed UTF-8 byte hmac.compare_digest replacement addresses that path.
Problems
- The added tests verify rejection and that the comparator is invoked, but do not verify a valid credential is accepted. Add a successful-auth assertion for
_handle_webhook. - Add non-ASCII credential coverage. The byte-based implementation supports it, and the related duplicate PR #9219 already demonstrates the useful correct/wrong credential cases.
Suggested changes
- Test a valid token returning 200 using a non-message webhook payload.
- Test a non-ASCII configured password with both matching and non-matching tokens.
Automated hermes-sweeper review.
| """Webhook credential check must be timing-safe (CWE-208).""" | ||
|
|
||
| def test_wrong_password_rejected(self, monkeypatch): | ||
| adapter = _make_adapter(monkeypatch) |
There was a problem hiding this comment.
Please add a positive-auth regression case. These tests establish 401 behavior and comparator invocation, but none verifies that a request with the configured token proceeds to the normal 200 webhook path.
Summary
The BlueBubbles webhook handler authenticated inbound requests with a plain string inequality check against the configured shared password. A plain
!=comparison short-circuits on the first differing byte, so the time taken to reject a request correlates with how many leading bytes of the supplied credential matched. This is a classic timing side channel (CWE-208) that, over many requests, can let an attacker recover the secret byte-by-byte.Root cause
gateway/platforms/bluebubbles.py—_handle_webhook:python token = request.query.get(password) or ... or request.headers.get(x-bluebubbles-guid) if token != self.password: return web.json_response({error: unauthorized}, status=401)Fix
Compare with
hmac.compare_digest(constant-time).tokenmay beNonewhen no credential is supplied, so it is coerced to an empty string before encoding; unauthenticated requests are still rejected and no exception is raised.Tests
tests/gateway/test_bluebubbles.py::TestBlueBubblesWebhookAuth— wrong credential rejected, missing credential rejected (no exception), and an invariant test asserting both operands flow throughhmac.compare_digest(i.e. no plain==fast path remains). Full existingtest_bluebubbles.pysuite (59 tests) still passes.Impact
Inbound webhook authentication only. No change to message handling, config, prompt cache, or role alternation. Behavior is identical for valid/invalid credentials apart from the comparison being constant-time.