fix(auth): bound local password encoding

This commit is contained in:
2026-08-16 20:03:40 +02:00
parent 1ed1a34ae2
commit de8844a4ac
2 changed files with 25 additions and 2 deletions
+3 -2
View File
@@ -59,8 +59,9 @@ function passwordBytes(password: string): Buffer | undefined {
} else if (/[\uD800-\uDFFF]/.test(password)) {
return undefined;
}
const bytes = Buffer.from(password, "utf8");
return bytes.length >= MINIMUM_PASSWORD_BYTES && bytes.length <= MAXIMUM_PASSWORD_BYTES ? bytes : undefined;
const byteLength = Buffer.byteLength(password, "utf8");
if (byteLength < MINIMUM_PASSWORD_BYTES || byteLength > MAXIMUM_PASSWORD_BYTES) return undefined;
return Buffer.from(password, "utf8");
}
export function isValidPasswordHash(encoded: string): boolean {
+22
View File
@@ -44,6 +44,28 @@ describe("local Argon2id password verification", () => {
expect(verifyPassword(loneSurrogatePassword, replacementPasswordHash)).toBe(false);
});
test("accepts a multibyte password at exactly the 1024-byte boundary", () => {
const password = "é".repeat(512);
const passwordHash = "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$LfO3M3JBKeXf1knenCjQ6m9z4B7nedb0As2uc522I1I";
expect(Buffer.byteLength(password, "utf8")).toBe(1024);
expect(verifyPassword(password, passwordHash)).toBe(true);
expect(verifyPassword(`${password}é`, passwordHash)).toBe(false);
});
test("rejects an over-limit password before converting it with Buffer.from", () => {
const password = "é".repeat(513);
const fromSpy = vi.spyOn(Buffer, "from");
try {
expect(Buffer.byteLength(password, "utf8")).toBe(1026);
expect(verifyPassword(password, vectors[0].phc)).toBe(false);
expect(fromSpy.mock.calls.some(([value, encoding]) => value === password && encoding === "utf8")).toBe(false);
} finally {
fromSpy.mockRestore();
}
});
test("rejects malformed and oversized PHC parameters before Argon2 allocation", () => {
const password = vectors[0].password;
const digest = vectors[0].phc.split("$")[5];