Repository navigation
sec(auth): constant-time token validation via subtle.ConstantTimeCompare (closes #392) - #837
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 58 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
|
@coderabbitai review |
Rate Limit Exceeded
|
ValidateSession had the same SQL-equality timing oracle as the user API key and password-reset token paths fixed in this PR: GetSession runs `WHERE token = $1` in PostgreSQL, which short-circuits on first mismatching byte. Session tokens are the hottest auth surface (every authenticated API request hits ValidateSession via handler.go), so the oracle is even more exploitable here than on the API key path. Add a `subtle.ConstantTimeCompare` of `session.Token` against the locally-derived `hashedToken` immediately after the store fetch, returning the same "session not found" error to keep the response indistinguishable from a missing-row outcome. `crypto/subtle` is already imported in service.go. Tests: - New `TestService_ValidateSession/constant-time mismatch rejects session even when DB row exists` asserts the guard fires when the stored token differs from the derived hash. - Fix `internal/server/adapter_test.go` fixtures that used literal stub tokens (`"hashed-token"`, `"hashed-session-token"`) to use a local `hashSessionTokenForTest` helper mirroring auth.hashSessionToken (private), so the new guard passes for the positive cases. `go test ./internal/...` -- 4223 passed, 0 failures.
Adversarial review (post-fix)Walked the risk surfaces in the brief; one in-scope gap fixed as
Other surfaces walked and verdicts:
Verification: @coderabbitai review |
|
Triggering the full review now. 🐇✨ 🧠 Learnings used✅ Action performedReview finished.
|
ValidateSession had the same SQL-equality timing oracle as the user API key and password-reset token paths fixed in this PR: GetSession runs `WHERE token = $1` in PostgreSQL, which short-circuits on first mismatching byte. Session tokens are the hottest auth surface (every authenticated API request hits ValidateSession via handler.go), so the oracle is even more exploitable here than on the API key path. Add a `subtle.ConstantTimeCompare` of `session.Token` against the locally-derived `hashedToken` immediately after the store fetch, returning the same "session not found" error to keep the response indistinguishable from a missing-row outcome. `crypto/subtle` is already imported in service.go. Tests: - New `TestService_ValidateSession/constant-time mismatch rejects session even when DB row exists` asserts the guard fires when the stored token differs from the derived hash. - Fix `internal/server/adapter_test.go` fixtures that used literal stub tokens (`"hashed-token"`, `"hashed-session-token"`) to use a local `hashSessionTokenForTest` helper mirroring auth.hashSessionToken (private), so the new guard passes for the positive cases. `go test ./internal/...` -- 4223 passed, 0 failures.
ValidateSession had the same SQL-equality timing oracle as the user API key and password-reset token paths fixed in this PR: GetSession runs `WHERE token = $1` in PostgreSQL, which short-circuits on first mismatching byte. Session tokens are the hottest auth surface (every authenticated API request hits ValidateSession via handler.go), so the oracle is even more exploitable here than on the API key path. Add a `subtle.ConstantTimeCompare` of `session.Token` against the locally-derived `hashedToken` immediately after the store fetch, returning the same "session not found" error to keep the response indistinguishable from a missing-row outcome. `crypto/subtle` is already imported in service.go. Tests: - New `TestService_ValidateSession/constant-time mismatch rejects session even when DB row exists` asserts the guard fires when the stored token differs from the derived hash. - Fix `internal/server/adapter_test.go` fixtures that used literal stub tokens (`"hashed-token"`, `"hashed-session-token"`) to use a local `hashSessionTokenForTest` helper mirroring auth.hashSessionToken (private), so the new guard passes for the positive cases. `go test ./internal/...` -- 4223 passed, 0 failures.
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
ValidateSession had the same SQL-equality timing oracle as the user API key and password-reset token paths fixed in this PR: GetSession runs `WHERE token = $1` in PostgreSQL, which short-circuits on first mismatching byte. Session tokens are the hottest auth surface (every authenticated API request hits ValidateSession via handler.go), so the oracle is even more exploitable here than on the API key path. Add a `subtle.ConstantTimeCompare` of `session.Token` against the locally-derived `hashedToken` immediately after the store fetch, returning the same "session not found" error to keep the response indistinguishable from a missing-row outcome. `crypto/subtle` is already imported in service.go. Tests: - New `TestService_ValidateSession/constant-time mismatch rejects session even when DB row exists` asserts the guard fires when the stored token differs from the derived hash. - Fix `internal/server/adapter_test.go` fixtures that used literal stub tokens (`"hashed-token"`, `"hashed-session-token"`) to use a local `hashSessionTokenForTest` helper mirroring auth.hashSessionToken (private), so the new guard passes for the positive cases. `go test ./internal/...` -- 4223 passed, 0 failures.
|
@coderabbitai review |
✅ Action performedReview finished.
|
ValidateSession had the same SQL-equality timing oracle as the user API key and password-reset token paths fixed in this PR: GetSession runs `WHERE token = $1` in PostgreSQL, which short-circuits on first mismatching byte. Session tokens are the hottest auth surface (every authenticated API request hits ValidateSession via handler.go), so the oracle is even more exploitable here than on the API key path. Add a `subtle.ConstantTimeCompare` of `session.Token` against the locally-derived `hashedToken` immediately after the store fetch, returning the same "session not found" error to keep the response indistinguishable from a missing-row outcome. `crypto/subtle` is already imported in service.go. Tests: - New `TestService_ValidateSession/constant-time mismatch rejects session even when DB row exists` asserts the guard fires when the stored token differs from the derived hash. - Fix `internal/server/adapter_test.go` fixtures that used literal stub tokens (`"hashed-token"`, `"hashed-session-token"`) to use a local `hashSessionTokenForTest` helper mirroring auth.hashSessionToken (private), so the new guard passes for the positive cases. `go test ./internal/...` -- 4223 passed, 0 failures.
|
@coderabbitai full review |
|
Merge-prep: rebased onto origin/main (27fdb06). One conflict in service_apikeys.go resolved: kept constant-time compare (PR security intent) + main's refactored err= style. go build/vet/lint/gocyclo all exit 0; auth tests pass (count=1); scheduledauth -count=5 clean. |
✅ Action performedFull review finished. You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 8 minutes. |
ValidateSession had the same SQL-equality timing oracle as the user API key and password-reset token paths fixed in this PR: GetSession runs `WHERE token = $1` in PostgreSQL, which short-circuits on first mismatching byte. Session tokens are the hottest auth surface (every authenticated API request hits ValidateSession via handler.go), so the oracle is even more exploitable here than on the API key path. Add a `subtle.ConstantTimeCompare` of `session.Token` against the locally-derived `hashedToken` immediately after the store fetch, returning the same "session not found" error to keep the response indistinguishable from a missing-row outcome. `crypto/subtle` is already imported in service.go. Tests: - New `TestService_ValidateSession/constant-time mismatch rejects session even when DB row exists` asserts the guard fires when the stored token differs from the derived hash. - Fix `internal/server/adapter_test.go` fixtures that used literal stub tokens (`"hashed-token"`, `"hashed-session-token"`) to use a local `hashSessionTokenForTest` helper mirroring auth.hashSessionToken (private), so the new guard passes for the positive cases. `go test ./internal/...` -- 4223 passed, 0 failures.
|
Rebased onto origin/main (89658a3) to pick up the #1437 tflint-cache and #1438 trivy-skip fixes. New tip: dd1cc4e. Resolved potential conflict in internal/auth/service_apikeys_test.go (main added TestService_HasAPIKeyPermissionAPI at line 797; PR adds a test case inside TestService_ValidateUserAPIKey at line 651 - separate chunks, clean merge). Local gates: go build/vet exit=0, gocyclo -over 10 on touched files exit=0, golangci-lint ./... exit=0 (no issues), go test ./internal/auth/... 595 passed, go test ./internal/server/... 410 passed. |
SQL equality in PostgreSQL is not constant-time; an attacker on the same VPC can use response-time differences to learn bytes of a SHA-256 hash. The admin API key path already uses subtle.ConstantTimeCompare; align the user API key and password-reset token paths to match. No schema changes: the stored hash format is unchanged. After the DB row is fetched, compare the expected hash against the stored hash via crypto/subtle.ConstantTimeCompare in ValidateUserAPIKey, validateResetToken, and ResetTokenStatus. A mismatch returns the same error as a missing row to avoid timing oracles via differing code paths. Update all test fixtures that set PasswordResetToken to literal stub values to instead use hashSessionToken(token) so they match the hash the service now verifies. Add targeted mismatch test cases to both the API-key and reset-token validation paths.
ValidateSession had the same SQL-equality timing oracle as the user API key and password-reset token paths fixed in this PR: GetSession runs `WHERE token = $1` in PostgreSQL, which short-circuits on first mismatching byte. Session tokens are the hottest auth surface (every authenticated API request hits ValidateSession via handler.go), so the oracle is even more exploitable here than on the API key path. Add a `subtle.ConstantTimeCompare` of `session.Token` against the locally-derived `hashedToken` immediately after the store fetch, returning the same "session not found" error to keep the response indistinguishable from a missing-row outcome. `crypto/subtle` is already imported in service.go. Tests: - New `TestService_ValidateSession/constant-time mismatch rejects session even when DB row exists` asserts the guard fires when the stored token differs from the derived hash. - Fix `internal/server/adapter_test.go` fixtures that used literal stub tokens (`"hashed-token"`, `"hashed-session-token"`) to use a local `hashSessionTokenForTest` helper mirroring auth.hashSessionToken (private), so the new guard passes for the positive cases. `go test ./internal/...` -- 4223 passed, 0 failures.
Session.Role was removed from the base branch; the constant-time mismatch test in service_test.go still referenced it, breaking go vet. Remove the field from the struct literal; the test logic is unaffected (it verifies token-hash rejection, not role checks).
ValidateUserAPIKey now runs subtle.ConstantTimeCompare(key.KeyHash, keyHash) after the DB fetch. The TestValidateUserAPIKey_LastUsedSingleflight fixture omitted KeyHash, so the empty stored hash failed the compare and the function returned early before the background UpdateAPIKeyLastUsed ran, breaking the mock's Once expectation. Real DB rows always carry KeyHash, so the fixture must set it to represent the actual validated path.
Summary
ValidateUserAPIKey: addsubtle.ConstantTimeCompareof the derived SHA-256/base64 hash against the storedkey_hashafter the DB row is fetched, closing the SQL equality timing oracle on the user API key pathvalidateResetToken+ResetTokenStatus: same pattern for password-reset tokens, comparinguser.PasswordResetTokenagainst the locally derivedtokenHash"hashed-token") to usehashSessionToken(token)so the new constant-time check passes; add two mismatch test cases to assert the guard rejects tampered rowsThe admin API key path in
middleware.goalready usedsubtle.ConstantTimeCompare; this PR brings the user API key and password-reset token paths to parity.Test plan
go test ./internal/auth/...passes (500 tests, 0 failures)go build ./...succeedsreject when stored hash does not match derived hashconfirms the API key guard firesconstant-time mismatch rejects token even when DB row existsconfirms the reset-token guard fires inConfirmPasswordResetconstant-time mismatch returns used even if DB row existsconfirms the guard fires inResetTokenStatus