security: webhook signature verification and input validation#310
security: webhook signature verification and input validation#310derekbarbosa wants to merge 5 commits into
Conversation
|
finished running some additional tests, triggering reviews via HMAC tokens appear to be working well. |
|
Gemini has some objections/findings: Since this gets passed to and uses |
|
Thank you for the review. I am curious why gemini did not pick this up with the review skill. fixing now, will run a few more passes of review locally before pushing V2. |
|
Idk, I always ask it to focus on potential regressions for the existing functionality and separately security concerns. Maybe we really need to spawn sashiko for sashiko? |
|
That would be actually cool! |
/me looks over at custom-prompts RFC 😄 |
|
v1 -> v2:
also, I had gemini do an analysis on some of the claims made Silent MBox Truncation SSRF Blocklist ^ considering webhooks are still "best effort" and this is documented in multiple places (as well as HMAC and/or SSL/TLS being recommended), I am veering towards "YAGNI" for making the blocklist more robust than it needs to be. Path Traversal via URL-encoded msgid |
|
My Gemini has few more findings, it looks like some of those are false positives based on your comments above, but not all of them. Please, take a look (1 and 3 in particular). Thanks! 1. Global 2MB Body Limit blocks large CLI submissions (Regression)Issue: axum::extract::DefaultBodyLimit::max(2 * 1024 * 1024) is applied globally to the Axum router. 2. Valid RFC-5322 Message-IDs are rejected (Regression)Issue: In fetch_and_inject_thread , clean_msgid.contains('/') rejects threads containing a forward or backward slash. 3. Reverse Proxy Authentication BypassIssue: The access control logic relies on checking addr.ip().is_loopback() . However, WEBHOOK_SECURITY.md explicitly recommends terminating TLS using a local 4. Overeager SSRF Blocklist False PositivesIssue: is_safe_repo_url() attempts to mitigate SSRF via basic substring matching, such as .contains("127.0.0.1") and .contains("0x7f000001") , against the entire |
…ints Add validation for untrusted input on HTTP ingestion endpoints: - Explicit 2 MiB DefaultBodyLimit on the axum router - Download size cap (10 MiB) and decompression limit (50 MiB) on lore.kernel.org mbox fetches to mitigate gzip bombs - SHA format validation (40 or 64 char hex) in both forge parse_payload methods to reject malformed commit hashes - Repository URL validation with SSRF blocklist (cloud metadata, loopback, localhost) in forge parse_payload - PR/MR number must be positive in both forge providers - Message-ID path separator sanitization in Thread submit handler SHA and URL validation are applied only in forge parse_payload (webhook path), not in submit_patch (CLI path), because the CLI legitimately sends git refs (HEAD, branch names, short SHAs) and local filesystem paths that are resolved server-side via git rev-parse. Assisted-by: claude-opus-4.6 <noreply@anthropic.com> Signed-off-by: derekbarbosa <derekasobrab@gmail.com>
Implement cryptographic verification for incoming forge webhook requests, mitigating forged patchset creation, cost amplification via unauthorized AI review triggers, and SSRF via injected repo URLs. Three authentication modes are supported, selected by header presence: - Standard Webhooks HMAC-SHA256 (GitLab 19.0+ signing token): verifies webhook-signature header over msg_id.timestamp.body - GitHub HMAC-SHA256: verifies X-Hub-Signature-256 header - Legacy GitLab secret token: constant-time comparison of X-Gitlab-Token header All comparisons use constant-time operations via the subtle crate to prevent timing-based secret extraction. The access control flow is revised: when webhook_secret is configured, non-localhost requests are permitted because the signature check is the access control. The --enable-unsafe-all-submit flag is only needed for unauthenticated setups. Startup warnings are emitted for insecure configurations. Secret type auto-detection: whsec_ prefix indicates a Standard Webhooks signing token (base64-decoded); plain strings are used directly for both X-Gitlab-Token and HMAC key bytes. A warning is logged if whsec_ prefix is present but base64 decoding fails. The ForgeProvider trait signature changes to accept body and optional secret, which is a breaking change for downstream trait implementors but not for API consumers. New dependencies: hmac 0.13, base64 0.22, subtle 2. Assisted-by: claude-opus-4.6 <noreply@anthropic.com> Signed-off-by: derekbarbosa <derekasobrab@gmail.com>
Add docs/WEBHOOK_SECURITY.md — a comprehensive guide covering webhook authentication setup for all deployment topologies: - Deployment topology decision tree (public server, tunnel, LAN, script-based) - Authentication method comparison (HMAC signing token, HMAC secret, plain secret token) - Step-by-step setup for GitLab signing tokens, GitLab legacy tokens, and GitHub HMAC - Script-based/cronjob curl examples for both plain and HMAC auth - Production deployment checklist - Reverse proxy examples (nginx with TLS/rate-limiting, Caddy) - Network security (GitLab/GitHub IP ranges, SSRF protections) - Secret management (env vars, file permissions, rotation) - Troubleshooting (every error code) and FAQ Update existing docs to replace placeholder security warnings: - FORGE_SETUP.md: replace security sections with summary + link, update comparison table to mark signature validation as implemented - GITHUB_SETUP.md: replace 'not yet implemented' with setup steps - GITLAB_SETUP.md: replace 'not validated' with signing token setup - configuration.md: add [forge] section and webhook_secret env var Add four topology-specific example configs: - Settings.forge-gitlab-production.toml (signing token + localhost) - Settings.forge-gitlab-simple.toml (plain secret + cronjob) - Settings.forge-github.toml (HMAC secret) - Settings.forge-selfhosted.toml (signing token + LAN bind) Update Settings.toml with documented webhook_secret guidance. Assisted-by: claude-opus-4.6 <noreply@anthropic.com> Signed-off-by: derekbarbosa <derekasobrab@gmail.com>
Add designs/DESIGN_WEBHOOK_INPUT_HARDENING.md per repository convention for auditability. Covers threat model, HMAC signature verification architecture, access control revision, body size limits, input validation, startup warnings, configuration, deployment topologies, and known risks. Assisted-by: claude-opus-4.6 <noreply@anthropic.com> Signed-off-by: derekbarbosa <derekasobrab@gmail.com>
- Return an explicit error when decompressed mbox hits the 50 MiB limit instead of silently truncating the stream - Raise DefaultBodyLimit from 2 MiB to 25 MiB to accommodate large CLI mbox submissions and GitHub webhook payloads (up to 25 MB) - Read decompressed data into Vec<u8> before UTF-8 conversion to avoid InvalidData errors when the byte limit splits a multi-byte character - Extend SSRF blocklist with decimal (2130706433) and hex (0x7f000001) representations of loopback addresses - Replace message-ID slash/backslash blocklist with percent-encoding via the percent-encoding crate, preserving RFC 5322 message-IDs that contain path-significant characters - Replace full-URL string matching in is_safe_repo_url with parsed host-only checks via the url crate, eliminating false positives from blocklist patterns appearing in usernames or paths - Restructure forge_webhook access control to make the security model explicit: when webhook_secret is configured, signature verification is the sole access control for ALL requests including loopback. The is_loopback bypass only applies when no secret is configured. This is critical for reverse proxy deployments where all traffic arrives from loopback. Assisted-by: gemini-3.1-pro Signed-off-by: derekbarbosa <derekasobrab@gmail.com>
34086c6 to
918ff15
Compare
|
v2 -> v3:
|
Harden all HTTP ingestion endpoints against untrusted input. This PR addresses four security gaps identified during an audit of the webhook and submit API attack surface:
Webhook HMAC signature verification
The ForgeProvider trait's validate_event now accepts the request body and an optional secret, enabling cryptographic verification of incoming webhooks. Three authentication methods are supported:
Secret type is auto-detected
whsec_ prefix triggers Standard Webhooks key decoding; plain strings are used directly. A warning is logged at startup if base64 decoding fails for a whsec_-prefixed token.
The access control in forge_webhook is revise
When webhook_secret is configured, non-localhost requests are permitted because the signature check IS the access control. --enable-unsafe-all-submit is only needed for unauthenticated setups.
Body and decompression size limits
An explicit DefaultBodyLimit::max(2 MiB) is added to the axum router. The fetch_and_inject_thread function gains a 10 MiB download cap and a 50 MiB decompression cap (via Read::take()) to mitigate gzip bombs from lore.kernel.org fetches.
SHA format validation -- is_valid_git_sha() validates that commit SHAs are 40 or 64 hex characters. Applied in both forge parse_payload methods. Not applied to the CLI submit path, which legitimately sends git refs and short SHAs.
URL validation and SSRF mitigation
is_safe_repo_url() rejects repository URLs targeting cloud metadata endpoints (169.254.*), loopback addresses, and other internal destinations. Applied in forge parse_payload methods. PR/MR numbers must be positive. Message-ID path separators are rejected in the Thread submit handler.
New dependencies: hmac 0.13, base64 0.22, subtle 2