From f1f64e6ff55b977d48c84bd2438ccfb720b735ef Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 28 Aug 2023 11:08:00 +0100 Subject: [PATCH] Fix flaky regex g flag causing unexpected validation password validation issue --- .../src/components/signup/UserInfoStep.tsx | 2 +- .../password/checkIsPasswordBreached.ts | 47 ++++++++++--------- .../checks/password/passwordRegexes.ts | 5 +- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 0e2b02f87..c09e43fcb 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -269,11 +269,11 @@ export default function UserInfoStep({ { - setPassword(pass); await checkPassword({ password: pass, setErrors }); + setPassword(pass); }} type="password" value={password} diff --git a/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts index 1a646d387..d978441a6 100644 --- a/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts @@ -1,5 +1,22 @@ import axios from "axios"; +// SHA-1 hash the password using the SubtleCrypto API +async function hashPassword(passwordBytes: ArrayBuffer): Promise { + 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 => { - 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 { - 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]|[^\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/,