fix(auth): prevent mask_secret panic on multi-byte UTF-8 secrets (#591)

Use char-based indexing instead of byte-offset slicing in mask_secret()
to prevent panics when secrets contain multi-byte UTF-8 characters
(accented letters, emoji, CJK, etc.).

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
This commit is contained in:
Justin Poehnelt
2026-03-23 12:00:16 -06:00
committed by GitHub
parent 01fb4f297c
commit d6794019f8
2 changed files with 23 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
Fix `mask_secret` panic on multi-byte UTF-8 secrets by using char-based indexing instead of byte-offset slicing
+18 -6
View File
@@ -22,17 +22,19 @@ use crate::error::GwsError;
/// Mask a secret string by showing only the first 4 and last 4 characters.
/// Strings with 8 or fewer characters are fully replaced with "***".
///
/// Uses char-based indexing (not byte offsets) so multi-byte UTF-8 secrets
/// never cause a panic.
fn mask_secret(s: &str) -> String {
const MASK_PREFIX_LEN: usize = 4;
const MASK_SUFFIX_LEN: usize = 4;
const MIN_LEN_FOR_PARTIAL_MASK: usize = MASK_PREFIX_LEN + MASK_SUFFIX_LEN;
if s.len() > MIN_LEN_FOR_PARTIAL_MASK {
format!(
"{}...{}",
&s[..MASK_PREFIX_LEN],
&s[s.len() - MASK_SUFFIX_LEN..]
)
let char_count = s.chars().count();
if char_count > MIN_LEN_FOR_PARTIAL_MASK {
let prefix: String = s.chars().take(MASK_PREFIX_LEN).collect();
let suffix: String = s.chars().skip(char_count - MASK_SUFFIX_LEN).collect();
format!("{prefix}...{suffix}")
} else {
"***".to_string()
}
@@ -2124,6 +2126,16 @@ mod tests {
assert_eq!(mask_secret("123456789"), "1234...6789");
}
#[test]
fn mask_secret_multibyte_utf8() {
// Multi-byte chars must not panic (previously used byte slicing)
assert_eq!(mask_secret("áéíóúñüÁÉÍÓÚ"), "áéíó...ÉÍÓÚ");
// Short multi-byte — should fully mask
assert_eq!(mask_secret("café"), "***");
// Exactly at boundary with multi-byte (9 Greek chars)
assert_eq!(mask_secret("αβγδεζηθι"), "αβγδ...ζηθι");
}
#[test]
fn find_unmatched_services_identifies_missing() {
let scopes = vec![