Skip to content

[FIX] Reject malformed or unexpected webhook signatures instead of raising - #1214

Open
SajalDevX wants to merge 6 commits into
CCExtractor:masterfrom
SajalDevX:fix/webhook-signature-parsing
Open

SajalDevX wants to merge 6 commits into
CCExtractor:masterfrom
SajalDevX:fix/webhook-signature-parsing

Conversation

@SajalDevX

@SajalDevX SajalDevX commented Sep 26, 2026 •

Copy link
Copy Markdown

In raising this pull request, I confirm the following (please check boxes):

  • I have read and understood the contributors guide.
  • I have checked that another pull request for this purpose does not exist.
  • I have considered, and confirmed that this submission will be valuable to others.
  • I accept that this submission may not be used, and the pull request closed at the will of the maintainer.
  • I give this submission freely, and claim no ownership to its content.

My familiarity with the project is as follows (check one):

  • I have never used the project.
  • I have used the project briefly.
  • I have used the project extensively, but have not contributed previously.
  • I am an active contributor to the project.

utility.is_valid_signature() raises on several X-Hub-Signature values instead of returning False, so the CI web hook answers 500 rather than the configured abort code:

Header Before
sha1 (no =) ValueError: not enough values to unpack
unknown=abc TypeError: Missing required argument 'digestmod'
new=abc TypeError (hashlib.new ends up as the digest)
sha1=é TypeError: comparing strings with non-ASCII characters is not supported

The algorithm was also looked up in hashlib.__dict__, so a correctly keyed md5=... signature was accepted too.

Change: only sha1 and sha256 are accepted, since those are the hashes GitHub signs with. Anything missing or malformed returns False, and the digests are compared as bytes.

Tests: two new tests in tests/test_utility.py, one for the accepted hashes and one for the rejected headers. The second fails on master. The full nose2 run passes (805 tests), as do isort, pydocstyle, pycodestyle and mypy.

I haven't changed which header is read. GitHub also sends X-Hub-Signature-256, and I'm happy to switch the web hook to prefer it in a follow-up if you'd like.

I used Claude to help find this and write the tests; I checked the change and ran the suite locally.

Update: SonarCloud flagged the SHA-1 signature path, so the check now reads X-Hub-Signature-256 instead of X-Hub-Signature. GitHub sends that header on every delivery that has a secret, so sha256 is the only algorithm accepted now; sha1, md5 and anything else are rejected. The test helpers sign with SHA-256 as well. tests.test_utility and tests.test_ci pass (249 tests).

is_valid_signature() raised instead of returning False for several
headers: "sha1" with no "=" (ValueError), an unknown algorithm name
(TypeError from hmac.new with digestmod=None), "new=..." (hashlib.new
used as the digest), and a non-ASCII digest (TypeError from
compare_digest on str). Each of these turned the CI web hook into a
500 instead of the configured abort code. Looking the algorithm up in
hashlib.__dict__ also meant any hashlib digest was accepted, e.g. md5.

Only accept sha1 and sha256, the hashes GitHub signs with, treat
anything malformed as an invalid signature, and compare the digests as
bytes.
X-Hub-Signature is always HMAC-SHA1. GitHub also sends X-Hub-Signature-256 on every delivery that has a secret, so only accept sha256 signatures and drop SHA-1 from the signature path.
@sonarqubecloud

Copy link
Copy Markdown

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