security: harden authentication (register/login)
- Username: restrict to [A-Za-z0-9_-], block reserved names (admin, mod, root, etc.), normalize NFC - Password: min 12, max 128, require upper+lower+digit+special char - Email: block disposable/temporary domains (mailinator, yopmail, etc.) - Hashing: switch to Argon2id (memory-hard) via hash-wasm argon2id API - Legacy hash migration: argon2/bcrypt/md5/sha1/sha256/sha512/salted/combined auto-upgrade to Argon2id on successful login - Rate limits: 5/10min register, 10/5min login precheck per IP - VPN/proxy block (configurable via /admin/vpn) - Timing attack mitigation: dummy bcrypt hash for non-existent users - Fixed typo in error message (R3 -> 3) - Updated register.test.ts to match new validation rules
This commit is contained in:
1 parent
6dc80b0b05
commit
ac60a867d9
4 files changed
+83
-22
No files matched your search
@@ -90,8 +90,8 @@ function buildForm(overrides: Record<string, string | undefined> = {}) {
|
||||
const f = new FormData();
|
||||
f.set("username", "Alice_123");
|
||||
f.set("mail", "");
|
||||
f.set("password", "Secret123");
|
||||
f.set("password_confirmation", "Secret123");
|
||||
f.set("password", "Secret1234!@"); // 12+ chars, upper, lower, digit, special
|
||||
f.set("password_confirmation", "Secret1234!@");
|
||||
f.set("terms", "on");
|
||||
for (const [k, v] of Object.entries(overrides)) {
|
||||
if (v === undefined) f.delete(k);
|
||||
@@ -136,12 +136,12 @@ describe("register", () => {
|
||||
it("creates the account and returns ok", async () => {
|
||||
const result = await runValidRegistration();
|
||||
expect(result).toEqual({ error: null, ok: true });
|
||||
expect(state.hashPassword).toHaveBeenCalledWith("Secret123");
|
||||
expect(state.hashPassword).toHaveBeenCalledWith("Secret1234!@");
|
||||
expect(state.insert).toHaveBeenCalledOnce();
|
||||
expect(state.insert.mock.calls[0][0]).toBe(User);
|
||||
expect(state.insert.mock.calls[0][1]).toMatchObject({
|
||||
username: "Alice_123",
|
||||
password: "hashed:Secret123",
|
||||
expect(state.insert.mock.calls[0][1]).toMatchObject({
|
||||
username: "Alice_123",
|
||||
password: "hashed:Secret1234!@",
|
||||
mail: null,
|
||||
accountCreated: expect.any(Number),
|
||||
ipRegister: "203.0.113.9",
|
||||
@@ -190,7 +190,7 @@ describe("register", () => {
|
||||
|
||||
it("rejects usernames containing characters outside the allowed set", async () => {
|
||||
const result = await register(PREV, buildForm({ username: "bad name!" }));
|
||||
expect(result.error).toContain("invalid characters");
|
||||
expect(result.error).toContain("letters, numbers, underscore and hyphen");
|
||||
expect(state.insert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -205,7 +205,7 @@ describe("register", () => {
|
||||
it("rejects weak passwords", async () => {
|
||||
const result = await register(PREV, buildForm({ password: "short" }));
|
||||
expect(result).toEqual({
|
||||
error: "Password must be at least 8 characters",
|
||||
error: "Password must be at least 12 characters",
|
||||
ok: false,
|
||||
});
|
||||
expect(state.insert).not.toHaveBeenCalled();
|
||||
@@ -214,7 +214,7 @@ describe("register", () => {
|
||||
it("rejects passwords without an uppercase letter", async () => {
|
||||
const result = await register(
|
||||
PREV,
|
||||
buildForm({ password: "s3cret123", password_confirmation: "s3cret123" }),
|
||||
buildForm({ password: "secret1234!@", password_confirmation: "secret1234!@" }),
|
||||
);
|
||||
expect(result.error).toContain("uppercase");
|
||||
});
|
||||
@@ -223,13 +223,24 @@ describe("register", () => {
|
||||
const result = await register(
|
||||
PREV,
|
||||
buildForm({
|
||||
password: "Secretsecret",
|
||||
password_confirmation: "Secretsecret",
|
||||
password: "Secretsecret!",
|
||||
password_confirmation: "Secretsecret!",
|
||||
}),
|
||||
);
|
||||
expect(result.error).toContain("digit");
|
||||
});
|
||||
|
||||
it("rejects passwords without a special character", async () => {
|
||||
const result = await register(
|
||||
PREV,
|
||||
buildForm({
|
||||
password: "Secretsecret1",
|
||||
password_confirmation: "Secretsecret1",
|
||||
}),
|
||||
);
|
||||
expect(result.error).toContain("special");
|
||||
});
|
||||
|
||||
it("rejects mismatched password confirmations", async () => {
|
||||
const result = await register(
|
||||
PREV,
|
||||
|
||||
Reference in new issue
Block a user