Use hashed webhook secrets for constant-time comparison (closes #13) #15
No reviewers
Labels
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: sneak/upaas#15
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch ":fix/issue-13"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Fixes the timing side-channel vulnerability in webhook secret lookup (issue #13).
Problem
FindAppByWebhookSecret()passed the user-supplied secret directly to a SQLWHERE webhook_secret = ?clause. SQL string comparison short-circuits on the first differing byte, enabling timing attacks that could reveal the secret character by character.Solution
Store a SHA-256 hash of the webhook secret in a new
webhook_secret_hashcolumn. On webhook receipt, hash the incoming secret and look up by hash. Since SHA-256 produces fixed-length output with avalanche properties, the SQL comparison time is independent of the original secret.Changes
webhook_secret_hashcolumndatabase.HashWebhookSecret(): SHA-256 hashing helperFindAppByWebhookSecret(): Now queries by hash instead of plaintextTestHashWebhookSecret, all existing tests updated and passingThe plaintext
webhook_secretcolumn is retained for display (webhook URL generation).Note
golangci-lintis not available in the build environment.go vetpasses cleanly.Test Output 1: Full test suite
All packages pass. No failures.
Test Output 2: Targeted tests (verbose)
Linting
golangci-lintnot available in build environment.go vet ./...passes cleanly with no warnings.Code Review: Hashed Webhook Secrets
Verdict: MERGE-READY
Clean, well-structured fix for the timing side-channel in webhook secret lookup.
Security Analysis
SHA-256 is sufficient here. The goal is not password storage (where you'd want bcrypt/argon2) -- it's to eliminate timing side-channels in SQL string comparison. SHA-256 produces fixed-length output with avalanche properties, making the SQL
WHERE webhook_secret_hash = ?comparison constant-time w.r.t. the original secret. The secrets are high-entropy UUIDs, so rainbow tables / brute-force are not practical concerns.Migration and Backfill
DEFAULT ''ensures existing rows get a valid value; no data loss riskbackfillWebhookSecretHashes()correctly collects rows before updating (avoids cursor issues), queries only rows needing backfill, logs each updateconnect(), so the column existsTransition Period
Fail-closed: if an app somehow has no hash (crash between migration and backfill),
FindAppByWebhookSecretsimply will not match. New apps compute hash at insert time. Backfill covers existing apps on next startup.Code Quality
HashWebhookSecretis clean, exported, pure functionappColumnsand scan order updated consistentlywebhook_secret_hash; update deliberately omits it (secret does not change) -- correctWebhookSecretHashviadatabase.HashWebhookSecret()Tests
TestHashWebhookSecret: determinism, uniqueness, known-value assertionTestAppFindByWebhookSecret: round-trip hash lookupMinor Observations (non-blocking)
webhook_secret_hash-- fine at current scale, worth adding if apps table grows significantlywebhook_secretretained for URL generation, documented in PR descriptionReady to merge.