From e8b611ede4f22bf4a3456319c17bc4a19ed5bdae Mon Sep 17 00:00:00 2001 From: Clupai8o0 Date: Sun, 30 Aug 2026 16:00:58 +1000 Subject: [PATCH] NEW-UXA11Y-11: require first AND last name to match (close a verification bypass) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review: name_matches accepted any non-empty ordered subset, so a single common token ("John" or "Doe") matched "John Michael Doe" — a student id plus one name token could claim the verified role for someone else. Now both the first and last tokens must match, middle tokens the student typed must appear in the roster in order (omitted middles still fine), and a single token, an arbitrary subset, or a wrong first/last name is rejected. Add tests: single-token input, wrong surname and wrong first name do NOT verify; full name and first+last with the middle omitted do. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE --- src/events/interaction_create.rs | 55 ++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/src/events/interaction_create.rs b/src/events/interaction_create.rs index b4f6de2..5fa1ea5 100644 --- a/src/events/interaction_create.rs +++ b/src/events/interaction_create.rs @@ -152,22 +152,40 @@ fn normalise_student_id(raw: &str) -> String { lowered.strip_prefix('s').unwrap_or(&lowered).to_string() } -/// True when the submitted name is the roster name, or every word the student -/// typed appears in the roster name in the same order. Lets "John Doe" match a -/// roster row of "John Michael Doe" without matching an unrelated person. +/// Whether the submitted name matches the roster name closely enough to be the +/// same person. +/// +/// The first and last name tokens must BOTH match, and any tokens the student +/// typed in between must appear in the roster name in order — so an omitted +/// middle name is fine, but a single token, an arbitrary subset, a reordered +/// name, or a wrong surname is not. This is deliberately strict: verification is +/// already weak identity evidence (a name plus a student id), and a looser rule +/// would let a student id plus one common name token ("John", "Doe") claim the +/// verified role for someone else. fn name_matches(roster: &str, submitted: &str) -> bool { let roster = normalise_name(roster); let submitted = normalise_name(submitted); - if submitted.is_empty() { + + let roster_words: Vec<&str> = roster.split_whitespace().collect(); + let submitted_words: Vec<&str> = submitted.split_whitespace().collect(); + + // A single token (or empty) is far too weak to identify a person, and a roster + // row without a distinct first and last name cannot be matched safely. + if submitted_words.len() < 2 || roster_words.len() < 2 { return false; } - if roster == submitted { - return true; + + // The first and last name must both match. + if submitted_words.first() != roster_words.first() + || submitted_words.last() != roster_words.last() + { + return false; } - let roster_words: Vec<&str> = roster.split(' ').collect(); + + // Every token the student typed must appear in the roster name in order. let mut idx = 0usize; - for word in submitted.split(' ') { - match roster_words[idx..].iter().position(|w| *w == word) { + for &word in &submitted_words { + match roster_words[idx..].iter().position(|&w| w == word) { Some(offset) => idx += offset + 1, None => return false, } @@ -386,11 +404,28 @@ mod tests { } #[test] - fn allows_a_missing_middle_name() { + fn verifies_full_name_and_omitted_middle() { + assert!(name_matches("John Michael Doe", "John Michael Doe")); assert!(name_matches("John Michael Doe", "John Doe")); assert!(name_matches("John Michael Doe", "john michael doe")); } + #[test] + fn rejects_a_single_token() { + // A student id plus one common name token must never verify. + assert!(!name_matches("John Michael Doe", "John")); + assert!(!name_matches("John Michael Doe", "Doe")); + assert!(!name_matches("John Michael Doe", "Michael")); + assert!(!name_matches("John Doe", "John")); + } + + #[test] + fn rejects_wrong_surname_or_first_name() { + assert!(!name_matches("John Michael Doe", "John Smith")); + assert!(!name_matches("John Michael Doe", "Jane Doe")); + assert!(!name_matches("John Doe", "Jack Doe")); + } + #[test] fn rejects_a_different_person() { assert!(!name_matches("John Michael Doe", "Jane Doe"));