You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
gchat: fail-closed JWT verification at construction (port of 9824d33) (#130)
* gchat: fail-closed JWT verification at construction (port of 9824d33)
Port the gchat slice of upstream chat 9824d33 (PR #441 "adapter hardening
pass") to GoogleChatAdapter. The constructor now fails closed: it raises
ValidationError unless at least one of the following gates webhook signature
verification --
- google_chat_project_number (or GOOGLE_CHAT_PROJECT_NUMBER), or
- pubsub_audience (or GOOGLE_CHAT_PUBSUB_AUDIENCE), or
- a new disable_signature_verification config flag explicitly set True
(with GOOGLE_CHAT_DISABLE_SIGNATURE_VERIFICATION=true env fallback).
Previously the adapter constructed in any state and silently accepted
unverified webhooks when no verifier was configured, allowing forged payloads
to impersonate users / trigger handlers. The new disable_signature_verification
flag is an explicit, dev-only escape hatch; when it is the sole reason the
adapter constructs, a warning is logged.
The bool/env resolution uses `x if x is not None else default` so an explicit
disable_signature_verification=False is distinct from unset and still fails
closed (matching upstream `config.x ?? env === "true"`).
Existing gchat test fixtures constructed the adapter without any gating field
and now break under fail-closed; updated those fixtures (helpers + inline
constructors) to set a valid gate (mostly the explicit opt-out, since they
exercise non-verification mechanics). Verified this is a fixture fix, not a
masked behavioral break: runtime webhook routing is unchanged in this slice.
Tracking: #98https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
* fix(gchat): per-shape verification + dataclass field order + test env isolation (codex review)
Addresses three findings from Codex's review of #130.
1. Per-shape webhook verification (P1, security).
handle_webhook accepts BOTH direct Google Chat webhooks AND Pub/Sub push
shapes on a single endpoint. The constructor's "at least one verifier OR
disable_signature_verification" check is a coarse gate: an operator who
configured only pubsub_audience left the direct-webhook shape going through
the warned-but-unverified path (and symmetrically for project_number vs
Pub/Sub). An attacker could pick the unconfigured shape to bypass the
configured verifier.
handle_webhook now REJECTS (401) any shape whose verifier isn't configured,
unless disable_signature_verification is explicitly set -- in which case
the existing warn-and-process path is preserved. Mirrors upstream
adapter-gchat/src/index.ts (vercel/chat) faithfully; upstream solves this
the same way (per-path require, construct-time stays any-of).
2. GoogleChatAdapterConfig.disable_signature_verification field order (P2).
The PR inserted the new field in the MIDDLE of the dataclass, shifting
every later positional arg. A caller like
GoogleChatAdapterConfig("creds", "audience", impersonate, logger)
would silently route `impersonate` into `disable_signature_verification`.
Moved the field to the END so existing positional callers keep working
(the field has a default so trailing positional callers are unaffected).
Added a regression test that pins the field's position via
dataclasses.fields().
3. Env-var leakage in fail-closed tests (P2, test isolation).
The env-fallback tests used a manual try/finally that only restored env
vars SET before the test. Setting
GOOGLE_CHAT_DISABLE_SIGNATURE_VERIFICATION=true in a test where the var
was originally absent leaked it to later tests, silently satisfying the
fail-closed gate in unrelated construction tests.
Converted to pytest's monkeypatch fixture, which restores both was-set
and was-absent cases at teardown. Added a load-bearing leak-detection
test that exercises the previously-leaky pattern within a single test.
All three findings have load-bearing tests added under tests/test_gchat_verification.py:
- TestPerShapeVerificationRejection (3 tests; first two would 200 instead of 401 pre-fix)
- TestDisableSignatureVerificationFieldOrder (2 tests; both fail pre-fix)
- TestDisableSignatureVerificationEnvFallback::test_env_does_not_leak_to_subsequent_construction
Validation: uv run ruff check, ruff format --check, audit_test_quality.py
(0 hard failures), full pytest (4063 passed, 3 skipped).
https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
---------
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments