Skip to content

fix(codebase): answer 401 rather than 500 on a non-ASCII webhook secret header - #1524

Merged
srtab merged 1 commit into
mainfrom
claude/webhook-non-ascii-header-500
Aug 22, 2026
Merged

fix(codebase): answer 401 rather than 500 on a non-ASCII webhook secret header#1524
srtab merged 1 commit into
mainfrom
claude/webhook-non-ascii-header-500

Conversation

@srtab

@srtab srtab commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Both webhook validators compared the incoming secret against the configured one using hmac.compare_digest on str values. That raises TypeError when either argument holds a non-ASCII character:

>>> import hmac; hmac.compare_digest("sécret", "sécret")
TypeError: comparing strings with non-ASCII characters is not supported

WSGI/ASGI header values reach Django latin-1-decoded, so any non-ASCII byte in the secret header becomes a non-ASCII str. An unauthenticated caller could therefore turn either webhook endpoint into a 500 — and a Sentry error event — with a one-byte header change, instead of the documented 401.

Both clients, not just GitLab

GitLab compares the X-Gitlab-Token header directly, so the path is immediate.

GitHub is less obvious and was worth checking: its startswith("sha256=") guard passes on a header whose remainder is non-ASCII, and that remainder (signature[7:]) is exactly what reaches compare_digest. So X-Hub-Signature-256: sha256=<0xE9> crashes it the same way.

The fix

Encode both sides to UTF-8 before comparing. This stays constant-time — compare_digest accepts bytes-like arguments and is designed for exactly this — and makes the 401 hold for every possible header value.

Unchanged: both validators still return True when no secret is configured. That is a separate, deliberate design choice and is out of scope here.

Tests

Three, each written failing first and each reproducing the real TypeError at the real call site before the fix:

  • test_a_non_ascii_token_is_rejected_rather_than_raising — GitLab validator returns False rather than raising
  • test_a_non_ascii_signature_is_rejected_rather_than_raising — GitHub validator, same
  • test_gitlab_callback_non_ascii_token_is_401_not_500 — HTTP level, asserts the endpoint answers 401; before the fix this test surfaced the crash rather than a status code, which is the user-visible half of the bug

GitHub has no view-level webhook test harness (its test_callbacks.py is unit-level), so no HTTP-level counterpart was invented for it; the function-level test covers the same defect.

make test → 4562 passing on this branch, ruff clean.

Provenance

Found while reviewing the Telegram notification channel (#1523), which hit the identical bug in its own webhook route and fixed it there. This backports the same fix to the two pre-existing validators.

…et header

hmac.compare_digest raises TypeError when either str argument holds a
non-ASCII character, and WSGI/ASGI header values reach Django latin-1
decoded. An unauthenticated caller could therefore turn either webhook
endpoint into a 500 (and a Sentry error event) with a one-byte header
change. Both validators now compare UTF-8 bytes, which stays constant-time
and makes the documented 401 hold for every possible header value.

The GitHub validator has the same shape: its sha256= prefix check passes on
a header whose remainder is non-ASCII, and the remainder is what gets
compared.

Leaves untouched the separate, deliberate choice to return True when no
secret is configured.
@srtab
srtab merged commit dcc3480 into main Aug 22, 2026
6 checks passed
@srtab
srtab deleted the claude/webhook-non-ascii-header-500 branch August 22, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant