You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ea8f266
Browse filesBrowse the repository at this point in the historyBrowse files
sec(auth): constant-time token validation via subtle.ConstantTimeCompare (closes#392) (#837)
* sec(auth): constant-time token check after DB fetch (closes#392)
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.
* sec(auth): constant-time session token check (extends #392 PR #837)
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.
* fix(auth): drop stale Role field from Session test literal
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).
* test(auth): set KeyHash in singleflight fixture for constant-time check
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.
0 commit comments