fix(webhook): accept Gitea's native bare-hex signature format

verify_signature compared the X-Gitea-Signature header against
"sha256=<hex>", which is GitHub's format. Gitea sends a bare 64-character
hex digest with no prefix, so a correctly-signed Gitea delivery could never
match and was rejected with 403 Invalid signature.

Confirmed by capturing a real Gitea delivery against a scratch listener:
  SIG_PRESENT=True  HAS_PREFIX=False  SIG_LEN=64

Now strips an optional "sha256=" prefix before comparing, so both Gitea's
native format and GitHub-style senders verify. The HMAC computation and the
constant-time comparison are unchanged — an absent, empty, or incorrect
signature is still rejected exactly as before.

This was the second of two faults blocking the webhook gate; the first was
the repository webhook pointing at the host's public IP, which Gitea's
ALLOWED_HOST_LIST denied before any request left the process.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude BM 2026-08-08 17:48:28 +00:00
parent 6d31b9bf2e
commit 29621c8837

View File

@ -29,11 +29,17 @@ SECRET = CONFIG.get("webhook_secret", "").encode()
def verify_signature(payload: bytes, signature: str) -> bool:
"""Verify Gitea webhook HMAC signature."""
"""Verify Gitea webhook HMAC signature.
Gitea sends X-Gitea-Signature as a bare hex digest. GitHub-style senders
prefix it with "sha256=". Accept either form — the HMAC comparison itself
is unchanged, so this does not loosen verification.
"""
if not SECRET:
return True # No secret configured — accept all
expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
provided = signature[len("sha256="):] if signature.startswith("sha256=") else signature
return hmac.compare_digest(expected, provided)
class WebhookHandler(BaseHTTPRequestHandler):