chore: remediate pygienium audit findings
Dead code: 77 verified-unused exports, files (BackArrow, MenuBars, cookies.ts, db/create.ts, schemas/comment.ts, security-headers.ts) and 12 unused dependencies removed Comments: ~370 RESTATE comments stripped across 53 files; 2 verbose blocks tightened; dead commented-out config removed Complexity: bulkUpsert extracted into 11 per-entity helpers (CCN 156->~10); login formHandler split into 3 submitters (CCN 63->~5); account page render split into 8 section components (CCN 40); updatePost SQL builder rebuilt; assert*Owned consolidated behind generic assertOwnedBy Defensive guards: 4 redundant rethrow/nullish guards removed
This commit is contained in:
@@ -21,7 +21,6 @@ describe("CSRF Protection", () => {
|
||||
const token = generateCSRFToken();
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe("string");
|
||||
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||
expect(token).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
);
|
||||
@@ -34,7 +33,6 @@ describe("CSRF Protection", () => {
|
||||
});
|
||||
|
||||
it("should generate cryptographically secure tokens", () => {
|
||||
// Generate multiple tokens and ensure no collisions
|
||||
const tokens = new Set<string>();
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
tokens.add(generateCSRFToken());
|
||||
@@ -50,7 +48,6 @@ describe("CSRF Protection", () => {
|
||||
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe("string");
|
||||
// Token should be a UUID
|
||||
expect(token).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
);
|
||||
@@ -122,7 +119,6 @@ describe("CSRF Protection", () => {
|
||||
const invalidToken1 = "b".repeat(36);
|
||||
const invalidToken2 = "b".repeat(35) + "a";
|
||||
|
||||
// Test timing for completely different tokens
|
||||
const event1 = createMockEvent({
|
||||
headers: { "x-csrf-token": invalidToken1 },
|
||||
cookies: { "csrf-token": validToken }
|
||||
@@ -132,7 +128,6 @@ describe("CSRF Protection", () => {
|
||||
validateCSRFToken(event1);
|
||||
const time1 = performance.now() - start1;
|
||||
|
||||
// Test timing for tokens that differ only at the end
|
||||
const event2 = createMockEvent({
|
||||
headers: { "x-csrf-token": invalidToken2 },
|
||||
cookies: { "csrf-token": validToken }
|
||||
@@ -142,7 +137,6 @@ describe("CSRF Protection", () => {
|
||||
validateCSRFToken(event2);
|
||||
const time2 = performance.now() - start2;
|
||||
|
||||
// Timing difference should be minimal (less than 1ms)
|
||||
// This tests for constant-time comparison
|
||||
const timeDiff = Math.abs(time1 - time2);
|
||||
expect(timeDiff).toBeLessThan(1);
|
||||
@@ -161,7 +155,6 @@ describe("CSRF Protection", () => {
|
||||
|
||||
describe("CSRF Attack Scenarios", () => {
|
||||
it("should prevent basic CSRF attack", () => {
|
||||
// Attacker doesn't have access to the CSRF token cookie
|
||||
const attackEvent = createMockEvent({
|
||||
headers: { "x-csrf-token": "attacker-guessed-token" }
|
||||
});
|
||||
@@ -174,7 +167,6 @@ describe("CSRF Protection", () => {
|
||||
const token1 = generateCSRFToken();
|
||||
const token2 = generateCSRFToken();
|
||||
|
||||
// User has token1, attacker tries to use token2
|
||||
const event = createMockEvent({
|
||||
headers: { "x-csrf-token": token2 },
|
||||
cookies: { "csrf-token": token1 }
|
||||
@@ -198,7 +190,6 @@ describe("CSRF Protection", () => {
|
||||
});
|
||||
|
||||
it("should prevent replay attacks with old tokens", () => {
|
||||
// Simulate an old token that was captured
|
||||
const oldToken = "old-captured-token-12345";
|
||||
|
||||
const event = createMockEvent({
|
||||
@@ -206,10 +197,8 @@ describe("CSRF Protection", () => {
|
||||
cookies: { "csrf-token": oldToken }
|
||||
});
|
||||
|
||||
// Even if tokens match, they should be validated by the system
|
||||
// This test validates the structure works correctly
|
||||
const isValid = validateCSRFToken(event);
|
||||
expect(isValid).toBe(true); // Matches are valid
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -270,9 +259,7 @@ describe("CSRF Protection", () => {
|
||||
tokens.push(generateCSRFToken());
|
||||
}
|
||||
|
||||
// Check for sequential patterns
|
||||
for (let i = 1; i < tokens.length; i++) {
|
||||
// Tokens should not be incrementing
|
||||
expect(tokens[i]).not.toBe(
|
||||
String(Number(tokens[i - 1].replace(/-/g, "")) + 1)
|
||||
);
|
||||
@@ -281,11 +268,9 @@ describe("CSRF Protection", () => {
|
||||
|
||||
it("should generate tokens with sufficient entropy", () => {
|
||||
const token = generateCSRFToken();
|
||||
// UUID without dashes should be 32 hex characters
|
||||
const hexString = token.replace(/-/g, "");
|
||||
expect(hexString).toMatch(/^[0-9a-f]{32}$/i);
|
||||
|
||||
// Check that not all characters are the same
|
||||
const uniqueChars = new Set(hexString.split(""));
|
||||
expect(uniqueChars.size).toBeGreaterThan(5);
|
||||
});
|
||||
@@ -299,7 +284,6 @@ describe("CSRF Protection", () => {
|
||||
}
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// Should generate 1000 tokens in less than 100ms
|
||||
expect(duration).toBeLessThan(100);
|
||||
});
|
||||
|
||||
@@ -316,7 +300,6 @@ describe("CSRF Protection", () => {
|
||||
}
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// Should validate 10000 tokens in less than 100ms
|
||||
expect(duration).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
@@ -402,7 +385,6 @@ describe("CSRF Protection", () => {
|
||||
const sessionAToken = generateCSRFToken();
|
||||
const sessionBToken = generateCSRFToken();
|
||||
|
||||
// Session A's cookie with Session B's header token
|
||||
const event = createMockEvent({
|
||||
headers: { "x-csrf-token": sessionBToken },
|
||||
cookies: { "csrf-token": sessionAToken }
|
||||
@@ -522,12 +504,10 @@ describe("CSRF Protection", () => {
|
||||
it("should issue CSRF token on setCSRFToken then validate it", () => {
|
||||
const event = createMockEvent({});
|
||||
|
||||
// Step 1: Login issues CSRF token
|
||||
const token = setCSRFToken(event);
|
||||
expect(token).toBeDefined();
|
||||
expect(typeof token).toBe("string");
|
||||
|
||||
// Step 2: Subsequent mutation sends token back
|
||||
const mutationEvent = createMockEvent({
|
||||
headers: { "x-csrf-token": token },
|
||||
cookies: { "csrf-token": token }
|
||||
@@ -538,7 +518,6 @@ describe("CSRF Protection", () => {
|
||||
});
|
||||
|
||||
it("should reject cross-origin POST without CSRF token", () => {
|
||||
// Simulated cross-site POST: attacker can read cookies but not set headers
|
||||
const attackEvent = createMockEvent({
|
||||
// No x-csrf-token header (cross-origin requests can't set custom headers)
|
||||
cookies: { "csrf-token": "victim-token" }
|
||||
|
||||
@@ -59,7 +59,6 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
for (const email of sqlEmails) {
|
||||
// Either reject as invalid, or it's properly escaped in queries
|
||||
const isValid = isValidEmail(email);
|
||||
// Test documents the behavior
|
||||
expect(typeof isValid).toBe("boolean");
|
||||
}
|
||||
});
|
||||
@@ -68,7 +67,6 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
const longEmail = "a".repeat(1000) + "@example.com";
|
||||
const result = isValidEmail(longEmail);
|
||||
|
||||
// Should handle gracefully
|
||||
expect(typeof result).toBe("boolean");
|
||||
});
|
||||
|
||||
@@ -130,7 +128,6 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
it("should use parameterized queries for user authentication", async () => {
|
||||
const conn = ConnectionFactory();
|
||||
|
||||
// Test that SQL injection attempts don't work
|
||||
const maliciousEmail = "admin'--";
|
||||
|
||||
try {
|
||||
@@ -140,7 +137,6 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
args: [maliciousEmail]
|
||||
});
|
||||
|
||||
// Should return no results (no user with that exact email)
|
||||
expect(result.rows.length).toBe(0);
|
||||
} catch (error) {
|
||||
// If error, ensure it's not a SQL error
|
||||
@@ -153,7 +149,6 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
|
||||
for (const payload of SQL_INJECTION_PAYLOADS) {
|
||||
try {
|
||||
// Test various injection points
|
||||
await conn.execute({
|
||||
sql: "SELECT * FROM User WHERE email = ?",
|
||||
args: [payload]
|
||||
@@ -164,7 +159,6 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
args: [payload]
|
||||
});
|
||||
|
||||
// Queries should complete without SQL errors
|
||||
expect(true).toBe(true);
|
||||
} catch (error: any) {
|
||||
// If error occurs, should not be SQL injection syntax error
|
||||
@@ -184,10 +178,8 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
args: [unionPayload]
|
||||
});
|
||||
|
||||
// Should not return password hashes
|
||||
if (result.rows.length > 0) {
|
||||
for (const row of result.rows) {
|
||||
// Ensure we don't get password_hash column
|
||||
expect(row).not.toHaveProperty("password_hash");
|
||||
}
|
||||
}
|
||||
@@ -200,7 +192,6 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
it("should prevent blind SQL injection timing attacks", async () => {
|
||||
const conn = ConnectionFactory();
|
||||
|
||||
// Timing-based payload
|
||||
const timingPayload = "admin' AND SLEEP(5)--";
|
||||
|
||||
const start = performance.now();
|
||||
@@ -210,18 +201,15 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
args: [timingPayload]
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore errors
|
||||
}
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// Should not delay for 5 seconds
|
||||
expect(duration).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
it("should prevent second-order SQL injection", async () => {
|
||||
const conn = ConnectionFactory();
|
||||
|
||||
// Store malicious data
|
||||
const maliciousName = "admin'--";
|
||||
|
||||
try {
|
||||
@@ -236,7 +224,6 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
]
|
||||
});
|
||||
|
||||
// Retrieve and use (should still be safe with parameterized queries)
|
||||
const result = await conn.execute({
|
||||
sql: "SELECT display_name FROM User WHERE email = ?",
|
||||
args: ["test-sqli@example.com"]
|
||||
@@ -244,13 +231,11 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
|
||||
expect(result.rows.length).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Cleanup
|
||||
await conn.execute({
|
||||
sql: "DELETE FROM User WHERE email = ?",
|
||||
args: ["test-sqli@example.com"]
|
||||
});
|
||||
} catch (error) {
|
||||
// Should not have SQL syntax errors
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
@@ -260,7 +245,6 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
it("should identify potentially dangerous XSS patterns", () => {
|
||||
// These payloads should be handled by frontend sanitization
|
||||
for (const payload of XSS_PAYLOADS) {
|
||||
// Document that these patterns exist
|
||||
expect(payload).toBeDefined();
|
||||
expect(typeof payload).toBe("string");
|
||||
|
||||
@@ -272,11 +256,9 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
it("should handle script tags in user input", () => {
|
||||
const scriptInput = "<script>alert('XSS')</script>";
|
||||
|
||||
// Validation should not crash
|
||||
const nameValid = isValidDisplayName(scriptInput);
|
||||
expect(typeof nameValid).toBe("boolean");
|
||||
|
||||
// Email validation
|
||||
const emailValid = isValidEmail(scriptInput);
|
||||
expect(typeof emailValid).toBe("boolean");
|
||||
});
|
||||
@@ -450,7 +432,6 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
|
||||
expect(typeof emailValid).toBe("boolean");
|
||||
expect(typeof nameValid).toBe("boolean");
|
||||
// Should complete quickly (no ReDoS)
|
||||
expect(duration).toBeLessThan(100);
|
||||
});
|
||||
|
||||
@@ -508,14 +489,12 @@ describe("Input Validation and Injection Prevention", () => {
|
||||
});
|
||||
|
||||
it("should not be vulnerable to ReDoS attacks", () => {
|
||||
// ReDoS payload with many repetitions
|
||||
const redosPayload = "a".repeat(1000) + "!";
|
||||
|
||||
const start = performance.now();
|
||||
validatePassword(redosPayload);
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// Should complete quickly
|
||||
expect(duration).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,6 @@ describe("Password Security", () => {
|
||||
|
||||
expect(hash).toBeDefined();
|
||||
expect(typeof hash).toBe("string");
|
||||
// Bcrypt hashes start with $2b$ or $2a$
|
||||
expect(hash).toMatch(/^\$2[ab]\$/);
|
||||
});
|
||||
|
||||
@@ -36,7 +35,6 @@ describe("Password Security", () => {
|
||||
const password = "TestPassword123!";
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
// Bcrypt hashes are 60 characters long
|
||||
expect(hash.length).toBe(60);
|
||||
});
|
||||
|
||||
@@ -132,32 +130,26 @@ describe("Password Security", () => {
|
||||
const password = "TestPassword123!";
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
// Measure time for correct password
|
||||
const { duration: correctDuration } = await measureTime(() =>
|
||||
checkPasswordSafe(password, hash)
|
||||
);
|
||||
|
||||
// Measure time for incorrect password
|
||||
const { duration: incorrectDuration } = await measureTime(() =>
|
||||
checkPasswordSafe("WrongPassword123!", hash)
|
||||
);
|
||||
|
||||
// Bcrypt comparison should take similar time regardless
|
||||
const timingDifference = Math.abs(correctDuration - incorrectDuration);
|
||||
|
||||
// Allow reasonable variance (bcrypt is inherently slow)
|
||||
expect(timingDifference).toBeLessThan(50);
|
||||
});
|
||||
|
||||
it("should handle null hash without timing leak", async () => {
|
||||
const password = "TestPassword123!";
|
||||
|
||||
// Measure time for null hash
|
||||
const { result: result1, duration: duration1 } = await measureTime(() =>
|
||||
checkPasswordSafe(password, null)
|
||||
);
|
||||
|
||||
// Measure time for undefined hash
|
||||
const { result: result2, duration: duration2 } = await measureTime(() =>
|
||||
checkPasswordSafe(password, undefined)
|
||||
);
|
||||
@@ -165,7 +157,6 @@ describe("Password Security", () => {
|
||||
expect(result1).toBe(false);
|
||||
expect(result2).toBe(false);
|
||||
|
||||
// Should take similar time
|
||||
const timingDifference = Math.abs(duration1 - duration2);
|
||||
expect(timingDifference).toBeLessThan(50);
|
||||
});
|
||||
@@ -178,7 +169,6 @@ describe("Password Security", () => {
|
||||
checkPasswordSafe(password, null)
|
||||
);
|
||||
|
||||
// Should take at least a few milliseconds (bcrypt is slow)
|
||||
expect(duration).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
@@ -186,12 +176,10 @@ describe("Password Security", () => {
|
||||
const password = "TestPassword123!";
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
// User exists
|
||||
const { duration: existsDuration } = await measureTime(() =>
|
||||
checkPasswordSafe("WrongPassword", hash)
|
||||
);
|
||||
|
||||
// User doesn't exist (null hash)
|
||||
const { duration: notExistsDuration } = await measureTime(() =>
|
||||
checkPasswordSafe("WrongPassword", null)
|
||||
);
|
||||
@@ -280,9 +268,9 @@ describe("Password Security", () => {
|
||||
});
|
||||
|
||||
it("should calculate password strength correctly", () => {
|
||||
const fairPassword = "MyP@ssw0rd12"; // 12 chars
|
||||
const goodPassword = "MyStr0ng!P@ssw0rd"; // 17 chars
|
||||
const strongPassword = "MyV3ry!Str0ng@P@ssw0rd123"; // 25 chars
|
||||
const fairPassword = "MyP@ssw0rd12";
|
||||
const goodPassword = "MyStr0ng!P@ssw0rd";
|
||||
const strongPassword = "MyV3ry!Str0ng@P@ssw0rd123";
|
||||
|
||||
expect(validatePassword(fairPassword).strength).toBe("fair");
|
||||
expect(validatePassword(goodPassword).strength).toBe("good");
|
||||
@@ -337,7 +325,6 @@ describe("Password Security", () => {
|
||||
const password = "TestPassword123!";
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
// Measure time for multiple checks (simulating brute force)
|
||||
const start = performance.now();
|
||||
const attempts = 10;
|
||||
|
||||
@@ -348,7 +335,6 @@ describe("Password Security", () => {
|
||||
const duration = performance.now() - start;
|
||||
const avgPerAttempt = duration / attempts;
|
||||
|
||||
// Each attempt should take significant time (bcrypt is slow)
|
||||
// This makes brute force impractical
|
||||
expect(avgPerAttempt).toBeGreaterThan(5); // At least 5ms per attempt
|
||||
});
|
||||
@@ -356,18 +342,15 @@ describe("Password Security", () => {
|
||||
it("should prevent rainbow table attacks with unique salts", async () => {
|
||||
const password = "CommonPassword123!";
|
||||
|
||||
// Generate multiple hashes for same password
|
||||
const hashes = await Promise.all(
|
||||
Array.from({ length: 10 }, () => hashPassword(password))
|
||||
);
|
||||
|
||||
// All hashes should be unique (different salts)
|
||||
const uniqueHashes = new Set(hashes);
|
||||
expect(uniqueHashes.size).toBe(10);
|
||||
});
|
||||
|
||||
it("should prevent password spraying with validation", () => {
|
||||
// Common passwords that should be rejected
|
||||
const commonPasswords = [
|
||||
"Password123!",
|
||||
"Welcome123!",
|
||||
@@ -382,7 +365,6 @@ describe("Password Security", () => {
|
||||
});
|
||||
|
||||
it("should resist dictionary attacks", () => {
|
||||
// Dictionary words that should be caught
|
||||
const dictionaryBased = ["Sunshine123!", "Princess456!", "Dragon789!@"];
|
||||
|
||||
for (const password of dictionaryBased) {
|
||||
@@ -394,7 +376,7 @@ describe("Password Security", () => {
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle very long passwords", async () => {
|
||||
const longPassword = "A1!a" + "x".repeat(1000); // Very long but valid
|
||||
const longPassword = "A1!a" + "x".repeat(1000);
|
||||
const hash = await hashPassword(longPassword);
|
||||
const match = await checkPassword(longPassword, hash);
|
||||
|
||||
@@ -413,7 +395,6 @@ describe("Password Security", () => {
|
||||
const hash = await hashPassword(nullBytePassword);
|
||||
const match = await checkPassword(nullBytePassword, hash);
|
||||
|
||||
// Behavior may vary - just ensure no crash
|
||||
expect(typeof match).toBe("boolean");
|
||||
});
|
||||
|
||||
@@ -450,7 +431,6 @@ describe("Password Security", () => {
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// Bcrypt should be slow enough to deter brute force
|
||||
// With 10 rounds, should take at least a few milliseconds
|
||||
expect(duration).toBeGreaterThan(5);
|
||||
// But not too slow for normal operation
|
||||
expect(duration).toBeLessThan(500);
|
||||
@@ -467,11 +447,9 @@ describe("Password Security", () => {
|
||||
durations.push(performance.now() - start);
|
||||
}
|
||||
|
||||
// Timing should be relatively consistent
|
||||
const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
|
||||
const maxDeviation = Math.max(...durations.map((d) => Math.abs(d - avg)));
|
||||
|
||||
// Allow reasonable variance
|
||||
expect(maxDeviation).toBeLessThan(avg * 0.5);
|
||||
});
|
||||
|
||||
@@ -484,7 +462,6 @@ describe("Password Security", () => {
|
||||
}
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// Validation is CPU-bound but should be fast
|
||||
expect(duration).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
@@ -494,12 +471,9 @@ describe("Password Security", () => {
|
||||
const password = "TestPassword123!";
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
// Check that hash uses correct salt rounds
|
||||
// Bcrypt format: $2b$rounds$salthash
|
||||
const parts = hash.split("$");
|
||||
const rounds = parseInt(parts[2]);
|
||||
|
||||
// Should use 10 rounds (from password.ts)
|
||||
expect(rounds).toBe(10);
|
||||
});
|
||||
|
||||
@@ -509,17 +483,14 @@ describe("Password Security", () => {
|
||||
Array.from({ length: 100 }, () => hashPassword(password))
|
||||
);
|
||||
|
||||
// Extract salts from hashes
|
||||
const salts = hashes.map((hash) => {
|
||||
const parts = hash.split("$");
|
||||
return parts[3].substring(0, 22); // Salt is 22 characters
|
||||
});
|
||||
|
||||
// All salts should be unique
|
||||
const uniqueSalts = new Set(salts);
|
||||
expect(uniqueSalts.size).toBe(100);
|
||||
|
||||
// Check for patterns in salts (should be random)
|
||||
for (let i = 1; i < salts.length; i++) {
|
||||
// Salts should not be sequential or predictable
|
||||
expect(salts[i]).not.toBe(salts[i - 1]);
|
||||
|
||||
@@ -60,12 +60,10 @@ describe("Rate Limiting", () => {
|
||||
const maxAttempts = 3;
|
||||
const windowMs = 60000;
|
||||
|
||||
// Use up all attempts
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
}
|
||||
|
||||
// Next attempt should throw
|
||||
try {
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
expect.unreachable("Should have thrown");
|
||||
@@ -79,7 +77,6 @@ describe("Rate Limiting", () => {
|
||||
const maxAttempts = 2;
|
||||
const windowMs = 60000;
|
||||
|
||||
// Use up all attempts
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
|
||||
@@ -99,12 +96,10 @@ describe("Rate Limiting", () => {
|
||||
const maxAttempts = 3;
|
||||
const windowMs = 500; // 500ms window for testing
|
||||
|
||||
// Use up all attempts
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
}
|
||||
|
||||
// Should be blocked immediately after
|
||||
try {
|
||||
await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
expect.unreachable("Should have thrown");
|
||||
@@ -112,10 +107,8 @@ describe("Rate Limiting", () => {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
|
||||
// Wait for window to expire
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
|
||||
// Should be allowed again
|
||||
const remaining = await checkRateLimit(identifier, maxAttempts, windowMs);
|
||||
expect(remaining).toBe(maxAttempts - 1);
|
||||
});
|
||||
@@ -125,13 +118,11 @@ describe("Rate Limiting", () => {
|
||||
const maxAttempts = 10;
|
||||
const windowMs = 60000;
|
||||
|
||||
// Simulate concurrent requests
|
||||
const results: number[] = [];
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
results.push(await checkRateLimit(identifier, maxAttempts, windowMs));
|
||||
}
|
||||
|
||||
// All should succeed with decreasing remaining counts
|
||||
expect(results).toEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]);
|
||||
});
|
||||
|
||||
@@ -142,12 +133,10 @@ describe("Rate Limiting", () => {
|
||||
const id1 = uniqueId("test1");
|
||||
const id2 = uniqueId("test2");
|
||||
|
||||
// Use up attempts for id1
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
await checkRateLimit(id1, maxAttempts, windowMs);
|
||||
}
|
||||
|
||||
// id1 should be blocked
|
||||
try {
|
||||
await checkRateLimit(id1, maxAttempts, windowMs);
|
||||
expect.unreachable("Should have thrown");
|
||||
@@ -155,7 +144,6 @@ describe("Rate Limiting", () => {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
|
||||
// id2 should still work
|
||||
const remaining = await checkRateLimit(id2, maxAttempts, windowMs);
|
||||
expect(remaining).toBe(maxAttempts - 1);
|
||||
});
|
||||
@@ -225,12 +213,10 @@ describe("Rate Limiting", () => {
|
||||
const email = `test-${Date.now()}@example.com`;
|
||||
|
||||
// IP rate limiting is skipped in test/dev, so only email limit applies
|
||||
// Use up email rate limit with same email
|
||||
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
|
||||
await rateLimitLogin(email, ip);
|
||||
}
|
||||
|
||||
// Next attempt should fail due to email limit
|
||||
try {
|
||||
await rateLimitLogin(email, ip);
|
||||
expect.unreachable("Should have thrown");
|
||||
@@ -242,12 +228,10 @@ describe("Rate Limiting", () => {
|
||||
it("should limit by email independently of IP", async () => {
|
||||
const email = `test-${Date.now()}@example.com`;
|
||||
|
||||
// Use different IPs but same email
|
||||
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
|
||||
await rateLimitLogin(email, randomIP());
|
||||
}
|
||||
|
||||
// Next attempt with different IP should still fail due to email limit
|
||||
try {
|
||||
await rateLimitLogin(email, randomIP());
|
||||
expect.unreachable("Should have thrown");
|
||||
@@ -260,13 +244,11 @@ describe("Rate Limiting", () => {
|
||||
const ip = randomIP();
|
||||
|
||||
// In test/dev, IP rate limiting is skipped
|
||||
// Should allow many different emails from same IP
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const email = `test${i}-${Date.now()}@example.com`;
|
||||
await rateLimitLogin(email, ip);
|
||||
}
|
||||
|
||||
// Should not throw since IP limits are disabled in test/dev
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -276,12 +258,10 @@ describe("Rate Limiting", () => {
|
||||
const ip = randomIP();
|
||||
|
||||
// IP rate limiting is skipped in test/dev
|
||||
// Should allow many attempts
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await rateLimitPasswordReset(ip);
|
||||
}
|
||||
|
||||
// Should not throw in test/dev
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
@@ -304,12 +284,10 @@ describe("Rate Limiting", () => {
|
||||
const ip = randomIP();
|
||||
|
||||
// IP rate limiting is skipped in test/dev
|
||||
// Should allow many attempts
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await rateLimitRegistration(ip);
|
||||
}
|
||||
|
||||
// Should not throw in test/dev
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -319,12 +297,10 @@ describe("Rate Limiting", () => {
|
||||
const ip = randomIP();
|
||||
|
||||
// IP rate limiting is skipped in test/dev
|
||||
// Should allow many attempts
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await rateLimitEmailVerification(ip);
|
||||
}
|
||||
|
||||
// Should not throw in test/dev
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -334,7 +310,6 @@ describe("Rate Limiting", () => {
|
||||
const email = "victim@example.com";
|
||||
const attackerIP = "1.2.3.4";
|
||||
|
||||
// Simulate brute force attack
|
||||
let blockedAtAttempt = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
@@ -347,7 +322,6 @@ describe("Rate Limiting", () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Should be blocked before 10 attempts
|
||||
expect(blockedAtAttempt).toBeLessThan(10);
|
||||
expect(blockedAtAttempt).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -355,7 +329,6 @@ describe("Rate Limiting", () => {
|
||||
it("should prevent distributed brute force from multiple IPs", async () => {
|
||||
const email = "victim@example.com";
|
||||
|
||||
// Simulate distributed attack from different IPs
|
||||
let blockedAtAttempt = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
@@ -368,7 +341,6 @@ describe("Rate Limiting", () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Should be blocked at email limit (3 attempts)
|
||||
expect(blockedAtAttempt).toBeLessThanOrEqual(
|
||||
RATE_LIMITS.LOGIN_EMAIL.maxAttempts
|
||||
);
|
||||
@@ -383,7 +355,6 @@ describe("Rate Limiting", () => {
|
||||
await rateLimitRegistration(attackerIP);
|
||||
}
|
||||
|
||||
// Should not block in test/dev (IP limits disabled)
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
@@ -396,7 +367,6 @@ describe("Rate Limiting", () => {
|
||||
await rateLimitPasswordReset(attackerIP);
|
||||
}
|
||||
|
||||
// Should not block in test/dev (IP limits disabled)
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -408,14 +378,12 @@ describe("Rate Limiting", () => {
|
||||
const unknownIP = "unknown";
|
||||
const email = `test-${Date.now()}@example.com`;
|
||||
|
||||
// Should allow many login attempts in development with unknown IP
|
||||
// (only email rate limit applies)
|
||||
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
|
||||
const testEmail = `test-${Date.now()}-${i}@example.com`;
|
||||
await rateLimitLogin(testEmail, unknownIP);
|
||||
}
|
||||
|
||||
// Should be able to continue with different emails (no IP limit in dev)
|
||||
await rateLimitLogin(`final-${Date.now()}@example.com`, unknownIP);
|
||||
});
|
||||
|
||||
@@ -423,12 +391,10 @@ describe("Rate Limiting", () => {
|
||||
const unknownIP = "unknown";
|
||||
const email = `test-${Date.now()}@example.com`;
|
||||
|
||||
// Use up email rate limit
|
||||
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
|
||||
await rateLimitLogin(email, unknownIP);
|
||||
}
|
||||
|
||||
// Next attempt should fail due to email limit
|
||||
try {
|
||||
await rateLimitLogin(email, unknownIP);
|
||||
expect.unreachable("Should have thrown");
|
||||
@@ -440,55 +406,46 @@ describe("Rate Limiting", () => {
|
||||
it("should handle unknown IP in password reset", async () => {
|
||||
const unknownIP = "unknown";
|
||||
|
||||
// In development, should allow many attempts (no IP limit)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await rateLimitPasswordReset(unknownIP);
|
||||
}
|
||||
|
||||
// Should not throw in development
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle unknown IP in registration", async () => {
|
||||
const unknownIP = "unknown";
|
||||
|
||||
// In development, should allow many attempts (no IP limit)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await rateLimitRegistration(unknownIP);
|
||||
}
|
||||
|
||||
// Should not throw in development
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle unknown IP in email verification", async () => {
|
||||
const unknownIP = "unknown";
|
||||
|
||||
// In development, should allow many attempts (no IP limit)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await rateLimitEmailVerification(unknownIP);
|
||||
}
|
||||
|
||||
// Should not throw in development
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rate Limit Configuration", () => {
|
||||
it("should have reasonable limits configured", () => {
|
||||
// Login should be more permissive than registration
|
||||
expect(RATE_LIMITS.LOGIN_IP.maxAttempts).toBeGreaterThan(
|
||||
RATE_LIMITS.REGISTRATION_IP.maxAttempts
|
||||
);
|
||||
|
||||
// All limits should be positive
|
||||
expect(RATE_LIMITS.LOGIN_IP.maxAttempts).toBeGreaterThan(0);
|
||||
expect(RATE_LIMITS.LOGIN_EMAIL.maxAttempts).toBeGreaterThan(0);
|
||||
expect(RATE_LIMITS.PASSWORD_RESET_IP.maxAttempts).toBeGreaterThan(0);
|
||||
expect(RATE_LIMITS.REGISTRATION_IP.maxAttempts).toBeGreaterThan(0);
|
||||
expect(RATE_LIMITS.EMAIL_VERIFICATION_IP.maxAttempts).toBeGreaterThan(0);
|
||||
|
||||
// All windows should be at least 1 minute
|
||||
expect(RATE_LIMITS.LOGIN_IP.windowMs).toBeGreaterThanOrEqual(60000);
|
||||
expect(RATE_LIMITS.LOGIN_EMAIL.windowMs).toBeGreaterThanOrEqual(60000);
|
||||
expect(RATE_LIMITS.PASSWORD_RESET_IP.windowMs).toBeGreaterThanOrEqual(
|
||||
@@ -552,7 +509,6 @@ describe("Rate Limiting", () => {
|
||||
const maxAttempts = 3;
|
||||
const windowMs = 60000;
|
||||
|
||||
// Exhaust the limit: 3 allowed, 4th blocked.
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
await checkRateLimit(id, maxAttempts, windowMs);
|
||||
}
|
||||
@@ -574,7 +530,6 @@ describe("Rate Limiting", () => {
|
||||
const maxAttempts = 5;
|
||||
const windowMs = 60000;
|
||||
|
||||
// Instance A: 3 attempts.
|
||||
clearRateLimitLocalCache();
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await checkRateLimit(id, maxAttempts, windowMs);
|
||||
@@ -582,9 +537,9 @@ describe("Rate Limiting", () => {
|
||||
|
||||
// Instance B (fresh local cache) makes 2 more -> combined count = 5.
|
||||
clearRateLimitLocalCache();
|
||||
await checkRateLimit(id, maxAttempts, windowMs); // count 4
|
||||
const remaining = await checkRateLimit(id, maxAttempts, windowMs); // count 5
|
||||
expect(remaining).toBe(0); // 5th allowed, no remaining
|
||||
await checkRateLimit(id, maxAttempts, windowMs);
|
||||
const remaining = await checkRateLimit(id, maxAttempts, windowMs);
|
||||
expect(remaining).toBe(0);
|
||||
|
||||
// A 6th attempt from a fresh instance must be blocked — the shared store
|
||||
// aggregated the count across the two "instances".
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
*/
|
||||
|
||||
import type { H3Event } from "vinxi/http";
|
||||
import { SignJWT } from "jose";
|
||||
import { env } from "~/env/server";
|
||||
|
||||
/**
|
||||
* Create a mock H3Event for testing
|
||||
@@ -62,55 +60,6 @@ export function createMockEvent(options: {
|
||||
return mockEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a valid JWT token for testing
|
||||
*/
|
||||
export async function createTestJWT(
|
||||
userId: string,
|
||||
expiresIn: string = "1h"
|
||||
): Promise<string> {
|
||||
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||
return await new SignJWT({ id: userId })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setExpirationTime(expiresIn)
|
||||
.sign(secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an expired JWT token for testing
|
||||
*/
|
||||
export async function createExpiredJWT(userId: string): Promise<string> {
|
||||
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||
return await new SignJWT({ id: userId })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setExpirationTime("-1h") // Expired 1 hour ago
|
||||
.sign(secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JWT with invalid signature
|
||||
*/
|
||||
export async function createInvalidSignatureJWT(
|
||||
userId: string
|
||||
): Promise<string> {
|
||||
const wrongSecret = new TextEncoder().encode("wrong-secret-key");
|
||||
return await new SignJWT({ id: userId })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setExpirationTime("1h")
|
||||
.sign(wrongSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate test credentials
|
||||
*/
|
||||
export function createTestCredentials() {
|
||||
return {
|
||||
email: `test-${Date.now()}@example.com`,
|
||||
password: "TestPass123!@#",
|
||||
passwordConfirmation: "TestPass123!@#"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Common SQL injection payloads
|
||||
*/
|
||||
@@ -138,26 +87,6 @@ export const XSS_PAYLOADS = [
|
||||
"<input onfocus=alert('XSS') autofocus>"
|
||||
];
|
||||
|
||||
/**
|
||||
* Wait for async operations with timeout
|
||||
*/
|
||||
export async function waitFor(
|
||||
condition: () => boolean | Promise<boolean>,
|
||||
timeout: number = 5000,
|
||||
interval: number = 100
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
if (await condition()) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||
}
|
||||
|
||||
throw new Error(`Timeout waiting for condition after ${timeout}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure execution time
|
||||
*/
|
||||
@@ -170,18 +99,6 @@ export async function measureTime<T>(
|
||||
return { result, duration };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random string for testing
|
||||
*/
|
||||
export function randomString(length: number = 10): string {
|
||||
const chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
return Array.from(
|
||||
{ length },
|
||||
() => chars[Math.floor(Math.random() * chars.length)]
|
||||
).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random IP address
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user