Skip to content

Use constant-time comparison for GitLab webhook secret - #692

Open
aegonmyy wants to merge 2 commits into
mattermost:masterfrom
aegonmyy:fix/webhook-timing-safe-compare
Open

Use constant-time comparison for GitLab webhook secret#692
aegonmyy wants to merge 2 commits into
mattermost:masterfrom
aegonmyy:fix/webhook-timing-safe-compare

Conversation

@aegonmyy

@aegonmyy aegonmyy commented Aug 3, 2026

Copy link
Copy Markdown

Summary

handleWebhook compares the configured WebhookSecret against the incoming X-Gitlab-Token header with a plain != string comparison. Go's string inequality is not constant-time (it can return as soon as it hits a differing byte), which leaks timing information about the secret on an endpoint that has no other authentication.

Fix: use subtle.ConstantTimeCompare, matching the pattern the sibling GitHub plugin already uses (hmac.Equal) for its own webhook signature check.

Test plan

  • Existing TestHandleWebhookBadSecret and the valid-secret webhook tests in server/webhook_test.go cover both branches; behavior is unchanged (only the comparison mechanism changed, not its outcome for equal/unequal inputs).

Change Impact: 🟡 Medium

Reasoning: The change affects webhook authentication but remains isolated to one module. Existing tests cover valid and invalid secrets, and the unauthorized response remains unchanged.

Regression Risk: Low. The comparison uses fixed-length SHA-256 digests and preserves the existing validation result.

** QA Recommendation:** Focused manual QA is recommended for valid, invalid, and near-match X-Gitlab-Token values. Skipping manual QA presents low functional risk.

Generated by CodeRabbitAI

handleWebhook compared the configured WebhookSecret against the
incoming X-Gitlab-Token header with a plain != check. Go's string
inequality is not constant-time, so this leaks byte-position timing
information about the secret on an endpoint that is intentionally
unauthenticated apart from this check. Switch to
subtle.ConstantTimeCompare, matching the pattern the sibling GitHub
plugin already uses (hmac.Equal) for its webhook signature check.
@aegonmyy
aegonmyy requested a review from a team as a code owner August 3, 2026 07:58
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f57b02a9-74ef-4243-8717-691b85167986

📥 Commits

Reviewing files that changed from the base of the PR and between a371c20 and 74dba17.

📒 Files selected for processing (1)
  • server/webhook.go

📝 Walkthrough

Walkthrough

Webhook authentication now compares SHA-256 digests with constant-time comparison. Unauthorized requests still receive a 401 response.

Changes

Webhook authentication

Layer / File(s) Summary
Constant-time secret validation
server/webhook.go
The webhook handler hashes both secrets with SHA-256 and compares the fixed-length digests with subtle.ConstantTimeCompare. The unauthorized response remains unchanged.

Estimated code review effort: 1 (Trivial) | ~3 minutes

Suggested reviewers: avasconcelos114

Poem

A rabbit guards the webhook gate,
Hashes secrets to check their state.
Constant time keeps clues away,
While 401s block the stray.
Hop, hop—secure today!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: using constant-time comparison for the GitLab webhook secret.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@aegonmyy aegonmyy closed this Aug 3, 2026
@aegonmyy aegonmyy reopened this Aug 3, 2026
Comment thread server/webhook.go Outdated

signature := r.Header.Get("X-Gitlab-Token")
if config.WebhookSecret != signature {
if subtle.ConstantTimeCompare([]byte(config.WebhookSecret), []byte(signature)) != 1 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remaining issue is that, unlike hmac.Equal(), the request's 'X-Gitlab-Token' can vary in it length, and subtle.ConstantTimeCompare returns 0 immediately if the two strings are different lengths. (And so you can at least determine the length of the secret.) So it seems we should keep going with this and compare digests of these two, i.e.:

secret := sha256.Sum256([]byte(config.WebhookSecret))
token := sha256.Sum256([]byte(signature))
if subtle.ConstantTimeCompare(secret[:], token[:]) != 1 {
	http.Error(w, "Not authorized", http.StatusUnauthorized)
	return
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, ConstantTimeCompare returns 0 immediately on a length mismatch, which leaks the length of the configured secret. Switched to comparing fixed-length SHA-256 digests of the secret and the incoming token so the comparison is constant time regardless of input length. Pushed in 74dba17.

@jgheithcock jgheithcock left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

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.

2 participants