Skip to content

Commit 888b500

Browse files
fix: block invisible and hidden blank characters in passwords
1 parent 8fd1768 commit 888b500

3 files changed

Lines changed: 40 additions & 6 deletions

File tree

src/shared/utils/passwordPatterns.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
// Unicode-aware patterns (the `u` flag enables \p{...} property escapes)
21
export const UPPERCASE_REGEXP = /\p{Lu}/u;
32
export const LOWERCASE_REGEXP = /\p{Ll}/u;
43
export const DIGIT_REGEXP = /\p{Nd}/u;
54
export const SYMBOL_REGEXP = /[^\p{L}\p{Nd}\s]/u;
6-
export const NO_WHITESPACE_REGEXP = /^\S+$/u;
7-
// Caseless scripts (CJK, Arabic, Hebrew, Korean, etc.) — letters with no upper/lower distinction
5+
// Allows only visible characters (letters, digits, punctuation, symbols, marks)
6+
export const VISIBLE_ONLY_REGEXP = /^[\p{L}\p{Nd}\p{P}\p{S}\p{M}]+$/u;
7+
// Blank-looking characters that Unicode classifies as visible
8+
export const HIDDEN_BLANKS_REGEXP = /[\u2800\u3164\u115F\u1160\uFFA0]/u;
9+
// Letters with no upper/lower distinction (CJK, Arabic, Hebrew, Korean, etc.)
810
export const CASELESS_LETTER_REGEXP = /\p{Lo}/u;
911

1012
export type PasswordCheckResult = {

src/shared/utils/passwordValidation.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,38 @@ describe('checkPassword', () => {
7575
it('returns false for empty string', () => {
7676
expect(checkPassword('').hasNoSpaces).toBe(false);
7777
});
78+
79+
it('returns false for zero-width space (U+200B)', () => {
80+
expect(checkPassword('abc\u200B123').hasNoSpaces).toBe(false);
81+
});
82+
83+
it('returns false for zero-width joiner (U+200D)', () => {
84+
expect(checkPassword('abc\u200D123').hasNoSpaces).toBe(false);
85+
});
86+
87+
it('returns false for zero-width non-joiner (U+200C)', () => {
88+
expect(checkPassword('abc\u200C123').hasNoSpaces).toBe(false);
89+
});
90+
91+
it('returns false for BOM / zero-width no-break space (U+FEFF)', () => {
92+
expect(checkPassword('abc\uFEFF123').hasNoSpaces).toBe(false);
93+
});
94+
95+
it('returns false for soft hyphen (U+00AD)', () => {
96+
expect(checkPassword('abc\u00AD123').hasNoSpaces).toBe(false);
97+
});
98+
99+
it('returns true for emoji (visible symbol)', () => {
100+
expect(checkPassword('abc😀123').hasNoSpaces).toBe(true);
101+
});
102+
103+
it('returns false for Braille blank (U+2800)', () => {
104+
expect(checkPassword('abc\u2800123').hasNoSpaces).toBe(false);
105+
});
106+
107+
it('returns false for Hangul filler (U+3164)', () => {
108+
expect(checkPassword('abc\u3164123').hasNoSpaces).toBe(false);
109+
});
78110
});
79111

80112
describe('meetsLength', () => {

src/shared/utils/passwordValidation.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
LOWERCASE_REGEXP,
66
DIGIT_REGEXP,
77
SYMBOL_REGEXP,
8-
NO_WHITESPACE_REGEXP,
8+
VISIBLE_ONLY_REGEXP,
9+
HIDDEN_BLANKS_REGEXP,
910
CASELESS_LETTER_REGEXP,
1011
type PasswordCheckResult,
1112
} from './passwordPatterns';
@@ -15,7 +16,6 @@ export type { PasswordCheckResult };
1516
export const checkPassword = (password: string): PasswordCheckResult => {
1617
const normalized = password.normalize('NFC');
1718
const hasCaselessLetter = CASELESS_LETTER_REGEXP.test(normalized);
18-
// Caseless scripts (CJK, Arabic, Hebrew, Korean) satisfy both upper and lower
1919
const hasUppercase = UPPERCASE_REGEXP.test(normalized) || hasCaselessLetter;
2020
const hasLowercase = LOWERCASE_REGEXP.test(normalized) || hasCaselessLetter;
2121
const hasDigit = DIGIT_REGEXP.test(normalized);
@@ -28,7 +28,7 @@ export const checkPassword = (password: string): PasswordCheckResult => {
2828
hasCaselessLetter,
2929
hasDigit,
3030
hasSymbol,
31-
hasNoSpaces: NO_WHITESPACE_REGEXP.test(normalized),
31+
hasNoSpaces: VISIBLE_ONLY_REGEXP.test(normalized) && !HIDDEN_BLANKS_REGEXP.test(normalized),
3232
meetsLength: [...normalized].length >= ACCOUNT_PASSWORD_MIN_LENGTH,
3333
charTypeCount,
3434
meetsCharTypeRequirement: charTypeCount >= ACCOUNT_PASSWORD_MIN_CHAR_TYPES,

0 commit comments

Comments
 (0)