Fix flaky regex g flag causing unexpected validation password validation issue

This commit is contained in:
Tuan Dang
2023-08-28 11:08:00 +01:00
parent a99751eb72
commit f1f64e6ff5
3 changed files with 28 additions and 26 deletions

View File

@@ -269,11 +269,11 @@ export default function UserInfoStep({
<InputField
label={t("section.password.password")}
onChangeHandler={async (pass: string) => {
setPassword(pass);
await checkPassword({
password: pass,
setErrors
});
setPassword(pass);
}}
type="password"
value={password}

View File

@@ -1,5 +1,22 @@
import axios from "axios";
// SHA-1 hash the password using the SubtleCrypto API
async function hashPassword(passwordBytes: ArrayBuffer): Promise<ArrayBuffer> {
const buffer = await window.crypto.subtle.digest("SHA-1", passwordBytes);
return buffer;
}
// Convert the hashed password buffer to a hexadecimal string
function bufferToHex(buffer: ArrayBuffer): string {
const byteArray = new Uint8Array(buffer);
const hexParts: string[] = [];
byteArray.forEach((byte) => {
const hex = byte.toString(16).padStart(2, "0");
hexParts.push(hex);
});
return hexParts.join("");
}
// see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange
// in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table
// this hash table is formed from the 5 char hash prefix (ie. 00000-FFFFF) so 16^5 results
@@ -21,7 +38,7 @@ import axios from "axios";
// thereof."
export const checkIsPasswordBreached = async (password: string): Promise<boolean> => {
const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/";
const HAVE_I_BEEN_PWNED_API_URL = "https://api.pwnedpasswords.com";
const maxRetryAttempts = 3;
let encodedPwd: Uint8Array | undefined;
@@ -32,34 +49,18 @@ export const checkIsPasswordBreached = async (password: string): Promise<boolean
const textEncoder = new TextEncoder();
encodedPwd = textEncoder.encode(password);
// SHA-1 hash the password using the SubtleCrypto API
async function hashPassword(passwordBytes: ArrayBuffer): Promise<ArrayBuffer> {
const buffer = await crypto.subtle.digest("SHA-1", passwordBytes);
return buffer;
}
// Convert the hashed password buffer to a hexadecimal string
function bufferToHex(buffer: ArrayBuffer): string {
const byteArray = new Uint8Array(buffer);
const hexParts: string[] = [];
byteArray.forEach((byte) => {
const hex = byte.toString(16).padStart(2, "0");
hexParts.push(hex);
});
return hexParts.join("");
}
// Hash the password and convert it to a useful format for the HIBP API
hashedPwdBuffer = await hashPassword(encodedPwd!.buffer);
const hashedPwd = bufferToHex(hashedPwdBuffer).toUpperCase();
// ONLY send the first 5 hash chars (over HTTPS)
const hashedPwdToSend = hashedPwd.slice(0, 5);
const safeHashedPwdToSend = encodeURIComponent(hashedPwdToSend); // Ensure URL safety
const rangedHashTableUri = `${dataBreachCheckAPIBaseURL}${safeHashedPwdToSend}`;
const rangedHashTableUri = `${HAVE_I_BEEN_PWNED_API_URL}/range/${safeHashedPwdToSend}`;
let response;
let retryAttempt = 0;
/* eslint-disable no-await-in-loop */
while (retryAttempt < maxRetryAttempts) {
try {
response = await axios.get(rangedHashTableUri, {
@@ -75,14 +76,14 @@ export const checkIsPasswordBreached = async (password: string): Promise<boolean
// check the last 35 hash chars to see if there's a match
const isBreachedPassword: boolean = responseData.includes(hashedPwd.slice(5, 40));
return isBreachedPassword;
} else {
retryAttempt++;
}
}
retryAttempt += 1;
} catch (err) {
if (!axios.isAxiosError(err)) {
throw err;
}
retryAttempt++;
retryAttempt += 1;
}
}

View File

@@ -1,8 +1,9 @@
// This regex covers letters (case insensitive) for the top 50 most spoken languages
/* eslint-disable no-misleading-character-class */
export const letterCharRegex = /[A-Za-z\u00C0-\u00D6\u00D8-\u00DE\u00DF-\u00F6\u00F8-\u00FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0600-\u06FF\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u05B0-\u05FF\u0980-\u09FF\u1F00-\u1FFF\u0130\u015E\u011E\u00C7\u00FC\u00FB\u00EB\u00E7]/u;
// This regex covers digits, special characters, symbols, and emojis.
export const numAndSpecialCharRegex = /[\d!@#$%^&*(),.?":{}|<>]|[^\p{L}\p{N}\s]/gu;
export const numAndSpecialCharRegex = /[\d!@#$%^&*(),.?":{}|<>]|[^\p{L}\p{N}\s]/u;
// This regex covers 3 repeated consecutive chars (incl. spaces)
export const repeatedCharRegex = /(.)\1\1\1|\s{4,}/;
@@ -19,7 +20,7 @@ export const lowEntropyRegexes = [
/^(?:(?:https?|ftp):\/\/)?(?:\w+\.)?[a-zA-Z0-9.-]+\.(?:com|org|net|edu)(?:\/\S*)?(?:\?\S*)?$/,
// Date in various formats
/(\b\d{1,4}[-\/.]?\d{1,2}[-\/.]?\d{1,4}\b)|(\b\d{1,4}[-\/.]?\w{3}[-\/.]?\d{1,4}\b)/,
/(\b\d{1,4}[-/.]?\d{1,2}[-/.]?\d{1,4}\b)|(\b\d{1,4}[-/.]?\w{3}[-/.]?\d{1,4}\b)/,
// Phone numbers (generalized)
/(?:\+(?:[1-9]\d{0,2})\s?)?(?:\(\d{1,4}\)\s?)?(?:\d[-.\s]?){5,}\d/,