mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-11-10 12:51:43 -10:00
### Why / What / How **Why.** While auditing the webhook ingress path, three issues surfaced: 1. **Exa** webhook verification was broken. The signature check ran inside `validate_payload`, computed HMAC against `webhook.secret` (Exa actually signs with the secret *it* returns at registration, stored in `config["exa_secret"]`), read the wrong header (`X-Exa-Signature` vs the real `Exa-Signature`), compared the raw `t=<ts>,v1=<hex>` header against a bare hex digest, and signed the body without the required `<timestamp>.` prefix. It could never have validated a real Exa delivery. 2. **Airtable** verification had the same flavor of bug: Airtable returns the signing secret base64-encoded as `macSecretBase64` (stored in `config["mac_secret"]`), but the code used the base64 string verbatim as the HMAC key instead of base64-decoding it first. Additionally, the cursor update in `validate_payload` *replaced* the whole `config` blob, wiping `mac_secret` after the first delivery. 3. The **generic webhook** trigger accepted any payload posted to its URL with no way to opt into a shared-secret check, even though the user controls both ends. These didn't outright break anything today because Exa/Airtable gated the check behind `if header_present:` — absent header → check skipped. Net effect: verification was effectively a no-op everywhere except GitHub/Telegram. **What.** Consolidate signature verification into a dedicated, provider-overridable step that runs *before* payload validation, and make it actually correct for every provider whose protocol supports it. **How.** New `BaseWebhooksManager.verify_signature(webhook, request)` classmethod, called by the ingress router before `validate_payload`. The default is a no-op so providers without a signing scheme (Compass, Slant3D) don't have to fake one. Verification failures return **403** (not 404 — that would leak webhook existence). Providers that sign override it and `hmac.compare_digest` against the stored secret. > **Note on feature flags:** an earlier revision of this PR gated Exa/Airtable enforcement behind LaunchDarkly flags (`ENFORCE_*`) for a staged rollout. We dropped that approach: a DB check confirmed **no agents use the Exa or Airtable webhook triggers** (zero registered webhooks for either provider), so the flags were pure ceremony — and a `default=False` gate makes a security control fail *open* on a LaunchDarkly outage. Verification now enforces unconditionally, consistent with GitHub/Telegram. ### Changes 🏗️ | File | Change | |---|---| | `webhooks/_base.py` | Add `verify_signature` classmethod (no-op default) | | `api/features/integrations/router.py` | Call `verify_signature` before `validate_payload`; 403 on failure | | `webhooks/github.py`, `telegram.py` | Move existing checks from `validate_payload` into `verify_signature` (pure refactor, behavior unchanged) | | `blocks/exa/_webhook.py` | Correct implementation: read `Exa-Signature`, parse `t=<ts>,v1=<hex>` (multi-`v1` supported), sign `<ts>.<raw body>`, key with `config["exa_secret"]` ([Exa docs](https://docs.exa.ai/websets/api/webhooks/verifying-signatures)) | | `blocks/airtable/_webhook.py` | Base64-decode `config["mac_secret"]` before HMAC; verify `X-Airtable-Content-MAC` (`hmac-sha256=<hex>`); merge cursor update into existing config instead of replacing it | | `blocks/generic_webhook/_webhook.py`, `triggers.py` | Optional `secret_token` input → require matching `X-Webhook-Secret` (constant-time); unset = today's behavior | | `docs/platform/new_blocks.md`, `docs/.../generic_webhook/triggers.md` | Document the `verify_signature` extension point and the generic `secret_token` | **Compass / Slant3D:** no code change — their protocols have no signing scheme; the default no-op covers them (the UUID URL is the bearer secret). ### Test plan 📋 `backend/api/features/integrations/webhook_ingress_test.py` (24 tests): - Unsigned providers (Compass, Slant3D) pass through. - Always-signed providers (GitHub, Telegram) reject missing/invalid sigs, accept valid ones. - **Exa**: missing / malformed / wrong-signature → 403; correct `t=,v1=` over `<ts>.<body>` → accepted; signature computed over body-only (the old bug) → rejected; missing `config["exa_secret"]` fail-closes. - **Airtable**: missing sig / missing `config["mac_secret"]` → 403; correct base64-decoded HMAC → accepted; regression test that the old "base64 string as key" HMAC does **not** verify. - **Generic webhook**: no-token / empty-token pass through; configured token → missing/wrong/correct `X-Webhook-Secret`. - Ordering: `verify_signature` runs before `validate_payload`. #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] `poetry run pytest backend/api/features/integrations/webhook_ingress_test.py` — 24 passed - [x] `poetry run lint` / `type-check` clean; full CI green Closes [SECRT-2359](https://linear.app/autogpt/issue/SECRT-2359). --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>