mirror of
https://github.com/stalwartlabs/stalwart.git
synced 2026-11-11 19:37:52 +00:00
Fix OAuth: Loopback redirect URIs without a path (for example http://127.0.0.1:45678) are not matched against registered loopback URIs on any port and are rejected by dynamic client registration
This commit is contained in:
1 parent
e27d49154f
commit
21e1831dee
3 files changed
+73
-9
No files matched your search
@@ -12,6 +12,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If
|
||||
|
||||
## Fixed
|
||||
- IMAP: `SEARCH RETURN` with `MIN` or `MAX` alongside `ALL` returns only the minimum and maximum in `ALL`, and `SAVE` combined with `MIN` or `MAX` and `ALL` or `COUNT` saves only the minimum and maximum instead of every match.
|
||||
- OAuth: Loopback redirect URIs without a path (for example `http://127.0.0.1:45678`) are not matched against registered loopback URIs on any port and are rejected by dynamic client registration.
|
||||
|
||||
## [0.16.26] - 2026-10-11
|
||||
|
||||
|
||||
@@ -212,15 +212,23 @@ pub fn loopback_redirect_parts(uri: &str) -> Option<(&str, &str)> {
|
||||
|
||||
for host in ["127.0.0.1", "[::1]"] {
|
||||
if let Some(rest) = uri.strip_prefix(host) {
|
||||
if let Some(path) = rest.strip_prefix('/') {
|
||||
return Some((host, path));
|
||||
} else if let Some(after_colon) = rest.strip_prefix(':')
|
||||
&& let Some((port, path)) = after_colon.split_once('/')
|
||||
&& !port.is_empty()
|
||||
&& port.bytes().all(|b| b.is_ascii_digit())
|
||||
{
|
||||
return Some((host, path));
|
||||
}
|
||||
let rest = match rest.strip_prefix(':') {
|
||||
Some(after_colon) => {
|
||||
let rest = after_colon.trim_start_matches(|c: char| c.is_ascii_digit());
|
||||
if rest.len() == after_colon.len() {
|
||||
return None;
|
||||
}
|
||||
rest
|
||||
}
|
||||
None => rest,
|
||||
};
|
||||
return if let Some(path) = rest.strip_prefix('/') {
|
||||
Some((host, path))
|
||||
} else if rest.is_empty() || rest.starts_with('?') {
|
||||
Some((host, rest))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -293,3 +301,55 @@ pub fn validate_grant_metadata(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{loopback_redirect_parts, redirect_uri_matches};
|
||||
|
||||
#[test]
|
||||
fn loopback_matches_any_port() {
|
||||
// Loopback redirects match regardless of port and of an empty versus "/" path (RFC 8252 §7.3)
|
||||
for (registered, presented) in [
|
||||
("http://127.0.0.1", "http://127.0.0.1:45678"),
|
||||
("http://127.0.0.1/", "http://127.0.0.1:45678"),
|
||||
("http://127.0.0.1", "http://127.0.0.1:45678/"),
|
||||
("http://127.0.0.1/cb", "http://127.0.0.1:45678/cb"),
|
||||
("http://127.0.0.1/?a=1", "http://127.0.0.1:45678?a=1"),
|
||||
("http://[::1]", "http://[::1]:8080"),
|
||||
("http://[::1]/", "http://[::1]:8080/"),
|
||||
] {
|
||||
assert!(
|
||||
redirect_uri_matches(registered, presented),
|
||||
"{registered} should match {presented}"
|
||||
);
|
||||
}
|
||||
|
||||
// Different paths, hosts or schemes never match
|
||||
for (registered, presented) in [
|
||||
("http://127.0.0.1/cb", "http://127.0.0.1:45678"),
|
||||
("http://127.0.0.1", "http://127.0.0.1:45678/cb"),
|
||||
("http://127.0.0.1", "http://[::1]:45678"),
|
||||
("http://127.0.0.1", "https://127.0.0.1:45678"),
|
||||
("http://127.0.0.1", "http://127.0.0.1.evil.com"),
|
||||
("http://127.0.0.1", "http://127.0.0.1:80@evil.com"),
|
||||
("http://127.0.0.1", "http://127.0.0.1:45678#frag"),
|
||||
] {
|
||||
assert!(
|
||||
!redirect_uri_matches(registered, presented),
|
||||
"{registered} should not match {presented}"
|
||||
);
|
||||
}
|
||||
|
||||
// Malformed loopback URIs are not recognised
|
||||
for uri in [
|
||||
"http://127.0.0.1:",
|
||||
"http://127.0.0.1:/cb",
|
||||
"http://127.0.0.1:abc/",
|
||||
"http://127.0.0.1.evil.com/",
|
||||
"http://127.0.0.1@evil.com/",
|
||||
"http://localhost:8080/",
|
||||
] {
|
||||
assert_eq!(loopback_redirect_parts(uri), None, "{uri}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,6 +246,9 @@ pub async fn test(test: &mut TestServer) {
|
||||
"http://127.0.0.1/cb",
|
||||
"http://127.0.0.1:54321/cb",
|
||||
"http://[::1]:8080/cb",
|
||||
"http://127.0.0.1",
|
||||
"http://127.0.0.1:54321",
|
||||
"http://[::1]:8080",
|
||||
] {
|
||||
let (status, body) = post_json_raw(
|
||||
&metadata.registration_endpoint,
|
||||
|
||||
Reference in new issue
Block a user