Compare a registration token's pinned email address ignoring case

The rest of MAS compares email addresses ignoring case. Comparing the pin
exactly made a token pinning `Alice@Example.com` unusable by a registration
whose address came from an upstream provider as `alice@example.com`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpxhUvvbRYpQSxhidPnnP7
This commit is contained in:
Devon Hudson
2026-10-13 15:30:54 +02:00
committed by Quentin Gliech
co-authored by Claude Opus 5.5
parent 6d30f3be66
commit 56f193dfbc
3 changed files with 72 additions and 2 deletions
+38 -1
View File
@@ -127,7 +127,11 @@ pub(super) async fn register(
state.add_error_on_field(RegisterFormField::Username, FieldError::Invalid);
}
if pinned_email.is_some_and(|pinned| Some(pinned) != email.as_deref()) {
if pinned_email.is_some_and(|pinned| {
!email
.as_deref()
.is_some_and(|email| pinned.eq_ignore_ascii_case(email))
}) {
state.add_error_on_field(RegisterFormField::Email, FieldError::Invalid);
}
@@ -1172,6 +1176,39 @@ mod tests {
assert!(!repo.user().exists("mallory").await.unwrap());
}
/// An invite code's pinned email address matches whatever its case
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_register_with_token_email_in_another_case(pool: PgPool) {
setup();
let state = TestState::from_pool(pool).await.unwrap();
let cookies = CookieHelper::new();
add_registration_token(
&state,
"invite_alice",
None,
Some("alice@example.com"),
false,
)
.await;
let csrf_token = mint_csrf_token(&state, &cookies);
let request = Request::post(&*mas_router::Register::default().path_and_query()).form(
serde_json::json!({
"csrf": csrf_token,
"username": "alice",
"email": "Alice@Example.com",
"password": "correcthorsebatterystaple",
"password_confirm": "correcthorsebatterystaple",
"token": "invite_alice",
"accept_terms": "on",
}),
);
let request = cookies.with_cookies(request);
let response = state.request(request).await;
response.assert_status(StatusCode::SEE_OTHER);
}
/// An invite code which pins an email address can't be used to register
/// another
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
@@ -26,7 +26,7 @@ fn token_fits(
&& token
.email
.as_deref()
.is_none_or(|pinned| Some(pinned) == email)
.is_none_or(|pinned| email.is_some_and(|email| pinned.eq_ignore_ascii_case(email)))
&& (token.passwordless
|| registration.password.is_some()
|| registration
@@ -325,6 +325,39 @@ mod tests {
);
}
/// A token's pinned email address matches whatever its case
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_accepts_token_pinning_the_email_in_another_case(pool: PgPool) {
setup();
let state = TestState::from_pool(pool).await.unwrap();
let cookies = CookieHelper::new();
add_registration_token(
&state,
"invite_alice",
None,
Some("Alice@Example.com"),
false,
)
.await;
let registration = add_registration(
&state,
&cookies,
"alice",
Some("alice@example.com"),
true,
None,
)
.await;
let response = submit_token(&state, &cookies, &registration, "invite_alice").await;
response.assert_status(StatusCode::SEE_OTHER);
response.assert_header_value(
LOCATION,
&mas_router::RegisterFinish::new(registration.id).path_and_query(),
);
}
/// A registration linked upstream needs no password, so any token fits it
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_accepts_token_not_passwordless_upstream(pool: PgPool) {