Skip to content

Commit 6c43448

Browse files
authored
security: use constant_eq for password checks (#1032)
Let's just be on par with Postgres itself on this https://www.postgresql.org/support/security/CVE-2026-6478/
1 parent 407a3c1 commit 6c43448

5 files changed

Lines changed: 42 additions & 6 deletions

File tree

pgdog/src/auth/md5.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ impl<'a> Client<'a> {
6767

6868
/// Check encrypted password against any of the configured passwords.
6969
pub fn check(&self, encrypted: &str) -> bool {
70-
self.passwords.iter().any(|p| self.encrypt(p) == encrypted)
70+
self.passwords.iter().any(|p| {
71+
crate::util::constant_time_eq(self.encrypt(p).as_bytes(), encrypted.as_bytes())
72+
})
7173
}
7274
}
7375

pgdog/src/backend/databases.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,14 @@ pub(crate) fn add(user: ConfigUser) -> Result<AuthResult, Error> {
181181
add_user(existing)?;
182182
reload_from_existing()?;
183183
Ok(AuthResult::Ok)
184-
} else if existing.password == user.password {
184+
} else if existing
185+
.password
186+
.as_deref()
187+
.zip(user.password.as_deref())
188+
.is_some_and(|(stored, provided)| {
189+
crate::util::constant_time_eq(stored.as_bytes(), provided.as_bytes())
190+
})
191+
{
185192
// Passwords match.
186193
Ok(AuthResult::Ok)
187194
} else if config.config.general.passthrough_auth.allows_change() {

pgdog/src/frontend/client/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,11 @@ impl Client {
195195
.await?;
196196
let response = stream.read().await?;
197197
let response = Password::from_bytes(response.to_bytes())?;
198-
let is_match = passwords
199-
.iter()
200-
.any(|p| Some(p.as_str()) == response.password());
198+
let is_match = response.password().is_some_and(|provided| {
199+
passwords.iter().any(|p| {
200+
crate::util::constant_time_eq(p.as_str().as_bytes(), provided.as_bytes())
201+
})
202+
});
201203

202204
if is_match {
203205
AuthResult::Ok

pgdog/src/net/messages/backend_key.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ impl SecretKey {
6565
/// byte. Length is not secret, so an early length mismatch returning `false`
6666
/// is fine.
6767
pub fn constant_time_eq(&self, other: &SecretKey) -> bool {
68-
aws_lc_rs::constant_time::verify_slices_are_equal(self.as_slice(), other.as_slice()).is_ok()
68+
crate::util::constant_time_eq(self.as_slice(), other.as_slice())
6969
}
7070

7171
pub fn len(&self) -> usize {

pgdog/src/util.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ pub fn millis(duration: Duration) -> f64 {
1717
(duration.as_secs_f64() * 1_000_000.0).round() / 1000.0
1818
}
1919

20+
/// Compare two byte slices in constant time with respect to their contents.
21+
///
22+
/// The running time depends only on the input lengths, never on the byte
23+
/// values, so it cannot leak (via a timing side channel) how many leading
24+
/// bytes matched. Use this wherever an attacker-supplied value is compared
25+
/// against a secret (passwords, cancel keys); a short-circuiting `==`/`memcmp`
26+
/// there is a covert timing channel that lets an attacker recover the secret
27+
/// byte by byte (cf. PostgreSQL CVE-2026-6478, the MD5 password comparison).
28+
///
29+
/// Length is not treated as secret: a length mismatch returns `false` early.
30+
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
31+
aws_lc_rs::constant_time::verify_slices_are_equal(a, b).is_ok()
32+
}
33+
2034
pub fn human_duration_optional(duration: Option<Duration>) -> String {
2135
if let Some(duration) = duration {
2236
human_duration(duration)
@@ -411,4 +425,15 @@ mod test {
411425
}
412426
assert_eq!(node_id(), Ok(1));
413427
}
428+
429+
#[test]
430+
fn test_constant_time_eq() {
431+
assert!(constant_time_eq(b"hunter2", b"hunter2"));
432+
assert!(!constant_time_eq(b"hunter2", b"hunter3"));
433+
// Different lengths must not match.
434+
assert!(!constant_time_eq(b"hunter2", b"hunter22"));
435+
assert!(!constant_time_eq(b"", b"x"));
436+
// Two empty slices are equal.
437+
assert!(constant_time_eq(b"", b""));
438+
}
414439
}

0 commit comments

Comments
 (0)