security hardening

This commit is contained in:
2025-12-28 20:04:29 -05:00
parent aefd467660
commit 1ba20339a8
22 changed files with 5177 additions and 116 deletions

View File

@@ -0,0 +1,486 @@
/**
* Authentication Security Tests
* Tests for authentication mechanisms including JWT, session management, and timing attacks
*/
import { describe, it, expect, beforeEach } from "bun:test";
import { getUserID, getPrivilegeLevel, checkAuthStatus } from "~/server/auth";
import {
createMockEvent,
createTestJWT,
createExpiredJWT,
createInvalidSignatureJWT,
measureTime
} from "./test-utils";
import { jwtVerify, SignJWT } from "jose";
import { env } from "~/env/server";
describe("Authentication Security", () => {
describe("JWT Token Validation", () => {
it("should validate correct JWT tokens", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBe(userId);
});
it("should reject expired JWT tokens", async () => {
const userId = "test-user-123";
const expiredToken = await createExpiredJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: expiredToken }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject JWT tokens with invalid signature", async () => {
const userId = "test-user-123";
const invalidToken = await createInvalidSignatureJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: invalidToken }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject malformed JWT tokens", async () => {
const event = createMockEvent({
cookies: { userIDToken: "not-a-valid-jwt" }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject empty JWT tokens", async () => {
const event = createMockEvent({
cookies: { userIDToken: "" }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject JWT tokens with missing user ID", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const tokenWithoutId = await new SignJWT({})
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: tokenWithoutId }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject JWT tokens with invalid user ID type", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const tokenWithNumberId = await new SignJWT({ id: 12345 })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: tokenWithNumberId }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should handle missing cookie gracefully", async () => {
const event = createMockEvent({});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
});
describe("JWT Token Tampering", () => {
it("should detect modified JWT payload", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
// Tamper with the payload (middle part of JWT)
const parts = token.split(".");
const tamperedPayload = Buffer.from(
JSON.stringify({ id: "attacker-id" })
).toString("base64url");
const tamperedToken = `${parts[0]}.${tamperedPayload}.${parts[2]}`;
const event = createMockEvent({
cookies: { userIDToken: tamperedToken }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should detect modified JWT signature", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
// Tamper with the signature (last part of JWT)
const parts = token.split(".");
const tamperedToken = `${parts[0]}.${parts[1]}.modified-signature`;
const event = createMockEvent({
cookies: { userIDToken: tamperedToken }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
it("should reject none algorithm JWT tokens", async () => {
// Try to create a token with 'none' algorithm (security vulnerability)
const payload = Buffer.from(
JSON.stringify({ id: "attacker-id", exp: Date.now() / 1000 + 3600 })
).toString("base64url");
const header = Buffer.from(
JSON.stringify({ alg: "none", typ: "JWT" })
).toString("base64url");
const noneToken = `${header}.${payload}.`;
const event = createMockEvent({
cookies: { userIDToken: noneToken }
});
const extractedUserId = await getUserID(event);
expect(extractedUserId).toBeNull();
});
});
describe("Privilege Level Security", () => {
it("should return admin privilege for admin user", async () => {
const adminId = env.ADMIN_ID;
const token = await createTestJWT(adminId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("admin");
});
it("should return user privilege for regular user", async () => {
const userId = "regular-user-123";
const token = await createTestJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("user");
});
it("should return anonymous privilege for unauthenticated request", async () => {
const event = createMockEvent({});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous");
});
it("should return anonymous privilege for invalid token", async () => {
const event = createMockEvent({
cookies: { userIDToken: "invalid-token" }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous");
});
it("should not allow privilege escalation through token manipulation", async () => {
const userId = "regular-user-123";
const token = await createTestJWT(userId);
// Even if attacker modifies the token, signature verification will fail
const parts = token.split(".");
const fakeAdminPayload = Buffer.from(
JSON.stringify({ id: env.ADMIN_ID })
).toString("base64url");
const fakeAdminToken = `${parts[0]}.${fakeAdminPayload}.${parts[2]}`;
const event = createMockEvent({
cookies: { userIDToken: fakeAdminToken }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous"); // Token validation fails
});
});
describe("Session Management", () => {
it("should identify authenticated sessions correctly", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const authStatus = await checkAuthStatus(event);
expect(authStatus.isAuthenticated).toBe(true);
expect(authStatus.userId).toBe(userId);
});
it("should identify unauthenticated sessions correctly", async () => {
const event = createMockEvent({});
const authStatus = await checkAuthStatus(event);
expect(authStatus.isAuthenticated).toBe(false);
expect(authStatus.userId).toBeNull();
});
it("should handle session with expired token", async () => {
const userId = "test-user-123";
const expiredToken = await createExpiredJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: expiredToken }
});
const authStatus = await checkAuthStatus(event);
expect(authStatus.isAuthenticated).toBe(false);
expect(authStatus.userId).toBeNull();
});
});
describe("Timing Attack Prevention", () => {
it("should have consistent timing for valid and invalid tokens", async () => {
const userId = "test-user-123";
const validToken = await createTestJWT(userId);
const invalidToken = "invalid-token";
// Measure time for valid token
const validEvent = createMockEvent({
cookies: { userIDToken: validToken }
});
const { duration: validDuration } = await measureTime(() =>
getUserID(validEvent)
);
// Measure time for invalid token
const invalidEvent = createMockEvent({
cookies: { userIDToken: invalidToken }
});
const { duration: invalidDuration } = await measureTime(() =>
getUserID(invalidEvent)
);
// Timing difference should be minimal (within reasonable variance)
// This helps prevent timing attacks to enumerate valid tokens
const timingDifference = Math.abs(validDuration - invalidDuration);
// Allow up to 5ms variance (accounts for system variations)
expect(timingDifference).toBeLessThan(5);
});
it("should have consistent timing for different user privilege levels", async () => {
const adminId = env.ADMIN_ID;
const userId = "regular-user-123";
const adminToken = await createTestJWT(adminId);
const userToken = await createTestJWT(userId);
// Measure time for admin privilege check
const adminEvent = createMockEvent({
cookies: { userIDToken: adminToken }
});
const { duration: adminDuration } = await measureTime(() =>
getPrivilegeLevel(adminEvent)
);
// Measure time for user privilege check
const userEvent = createMockEvent({
cookies: { userIDToken: userToken }
});
const { duration: userDuration } = await measureTime(() =>
getPrivilegeLevel(userEvent)
);
const timingDifference = Math.abs(adminDuration - userDuration);
expect(timingDifference).toBeLessThan(5);
});
});
describe("Token Expiration", () => {
it("should respect token expiration time", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const userId = "test-user-123";
// Create token expiring in 1 second
const shortLivedToken = await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1s")
.sign(secret);
// Should work immediately
const event1 = createMockEvent({
cookies: { userIDToken: shortLivedToken }
});
const id1 = await getUserID(event1);
expect(id1).toBe(userId);
// Wait for token to expire
await new Promise((resolve) => setTimeout(resolve, 1500));
// Should fail after expiration
const event2 = createMockEvent({
cookies: { userIDToken: shortLivedToken }
});
const id2 = await getUserID(event2);
expect(id2).toBeNull();
});
it("should handle tokens with very long expiration", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const userId = "test-user-123";
const longLivedToken = await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("365d") // 1 year
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: longLivedToken }
});
const extractedId = await getUserID(event);
expect(extractedId).toBe(userId);
});
it("should reject tokens with past expiration timestamps", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const userId = "test-user-123";
const pastToken = await new SignJWT({ id: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(Math.floor(Date.now() / 1000) - 3600) // 1 hour ago
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: pastToken }
});
const extractedId = await getUserID(event);
expect(extractedId).toBeNull();
});
});
describe("Edge Cases", () => {
it("should handle very long JWT tokens", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const largePayload = {
id: "test-user-123",
extraData: "x".repeat(10000) // 10KB of extra data
};
const largeToken = await new SignJWT(largePayload)
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: largeToken }
});
const extractedId = await getUserID(event);
expect(extractedId).toBe("test-user-123");
});
it("should handle special characters in user IDs", async () => {
const specialUserId = "user-with-special-!@#$%^&*()";
const token = await createTestJWT(specialUserId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const extractedId = await getUserID(event);
expect(extractedId).toBe(specialUserId);
});
it("should handle unicode user IDs", async () => {
const unicodeUserId = "user-with-unicode-🔐🛡️";
const token = await createTestJWT(unicodeUserId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const extractedId = await getUserID(event);
expect(extractedId).toBe(unicodeUserId);
});
it("should reject JWT with future issued-at time", async () => {
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const futureToken = await new SignJWT({ id: "test-user-123" })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt(Math.floor(Date.now() / 1000) + 3600) // 1 hour in future
.setExpirationTime("2h")
.sign(secret);
const event = createMockEvent({
cookies: { userIDToken: futureToken }
});
// Some JWT libraries reject future iat, some don't
// This test documents the behavior
const extractedId = await getUserID(event);
// Behavior may vary - just ensure no crash
expect(extractedId === null || extractedId === "test-user-123").toBe(
true
);
});
});
describe("Performance", () => {
it("should validate tokens efficiently", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const start = performance.now();
for (let i = 0; i < 1000; i++) {
await getUserID(event);
}
const duration = performance.now() - start;
// Should validate 1000 tokens in less than 100ms
expect(duration).toBeLessThan(100);
});
it("should check privilege levels efficiently", async () => {
const userId = "test-user-123";
const token = await createTestJWT(userId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const start = performance.now();
for (let i = 0; i < 1000; i++) {
await getPrivilegeLevel(event);
}
const duration = performance.now() - start;
// Should check 1000 privileges in less than 100ms
expect(duration).toBeLessThan(100);
});
});
});

View File

@@ -0,0 +1,417 @@
/**
* Authorization Tests
* Tests for access control, privilege escalation prevention, and admin access
*/
import { describe, it, expect } from "bun:test";
import { getUserID, getPrivilegeLevel } from "~/server/auth";
import { createMockEvent, createTestJWT } from "./test-utils";
import { env } from "~/env/server";
describe("Authorization", () => {
describe("Admin Access Control", () => {
it("should grant admin access to configured admin user", async () => {
const adminToken = await createTestJWT(env.ADMIN_ID);
const event = createMockEvent({
cookies: { userIDToken: adminToken }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("admin");
});
it("should deny admin access to regular users", async () => {
const userToken = await createTestJWT("regular-user-123");
const event = createMockEvent({
cookies: { userIDToken: userToken }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("user");
expect(privilege).not.toBe("admin");
});
it("should deny admin access to anonymous users", async () => {
const event = createMockEvent({});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous");
expect(privilege).not.toBe("admin");
});
it("should not allow privilege escalation through token tampering", async () => {
// Create a regular user token
const regularToken = await createTestJWT("regular-user-123");
// Attacker tries to modify token to include admin ID
// This should fail signature verification
const parts = regularToken.split(".");
const fakeAdminPayload = Buffer.from(
JSON.stringify({ id: env.ADMIN_ID })
).toString("base64url");
const tamperedToken = `${parts[0]}.${fakeAdminPayload}.${parts[2]}`;
const event = createMockEvent({
cookies: { userIDToken: tamperedToken }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous"); // Invalid token = anonymous
});
it("should handle malformed admin ID gracefully", async () => {
const invalidIds = ["", null, undefined, " ", "admin'--"];
for (const invalidId of invalidIds) {
const token = await createTestJWT(invalidId as string);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const privilege = await getPrivilegeLevel(event);
// Should not grant admin access for invalid IDs
expect(privilege).not.toBe("admin");
}
});
});
describe("User Access Control", () => {
it("should grant user access to authenticated users", async () => {
const userToken = await createTestJWT("user-123");
const event = createMockEvent({
cookies: { userIDToken: userToken }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("user");
});
it("should deny user access to anonymous requests", async () => {
const event = createMockEvent({});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous");
expect(privilege).not.toBe("user");
});
it("should maintain user access with valid token", async () => {
const userToken = await createTestJWT("user-456");
const event = createMockEvent({
cookies: { userIDToken: userToken }
});
const userId = await getUserID(event);
expect(userId).toBe("user-456");
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("user");
});
});
describe("Privilege Escalation Prevention", () => {
it("should prevent horizontal privilege escalation", async () => {
const user1Token = await createTestJWT("user-1");
const user2Token = await createTestJWT("user-2");
const event1 = createMockEvent({
cookies: { userIDToken: user1Token }
});
const event2 = createMockEvent({
cookies: { userIDToken: user2Token }
});
const user1Id = await getUserID(event1);
const user2Id = await getUserID(event2);
expect(user1Id).toBe("user-1");
expect(user2Id).toBe("user-2");
expect(user1Id).not.toBe(user2Id);
});
it("should prevent vertical privilege escalation", async () => {
// Regular user should not be able to become admin
const userToken = await createTestJWT("regular-user");
const event = createMockEvent({
cookies: { userIDToken: userToken }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("user");
// Even with multiple checks, privilege should remain the same
const privilege2 = await getPrivilegeLevel(event);
expect(privilege2).toBe("user");
});
it("should not allow session hijacking through token reuse", async () => {
const user1Token = await createTestJWT("user-1");
// User 1's token should always return user 1's ID
const event1 = createMockEvent({
cookies: { userIDToken: user1Token }
});
const id1 = await getUserID(event1);
// Even if attacker captures token, it still identifies as user 1
const event2 = createMockEvent({
cookies: { userIDToken: user1Token }
});
const id2 = await getUserID(event2);
expect(id1).toBe("user-1");
expect(id2).toBe("user-1");
});
it("should prevent privilege escalation via race conditions", async () => {
const userToken = await createTestJWT("concurrent-user");
const event = createMockEvent({
cookies: { userIDToken: userToken }
});
// Simulate concurrent privilege checks
const results = await Promise.all([
getPrivilegeLevel(event),
getPrivilegeLevel(event),
getPrivilegeLevel(event),
getPrivilegeLevel(event),
getPrivilegeLevel(event)
]);
// All results should be the same
expect(results.every((r) => r === "user")).toBe(true);
});
});
describe("Anonymous Access", () => {
it("should handle missing authentication token", async () => {
const event = createMockEvent({});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous");
});
it("should handle empty authentication token", async () => {
const event = createMockEvent({
cookies: { userIDToken: "" }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous");
});
it("should handle invalid token format", async () => {
const event = createMockEvent({
cookies: { userIDToken: "not-a-jwt-token" }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous");
});
it("should return null user ID for anonymous users", async () => {
const event = createMockEvent({});
const userId = await getUserID(event);
expect(userId).toBeNull();
});
});
describe("Access Control Edge Cases", () => {
it("should handle user ID with special characters", async () => {
const specialUserId = "user-with-special-!@#$%";
const token = await createTestJWT(specialUserId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const userId = await getUserID(event);
expect(userId).toBe(specialUserId);
});
it("should handle very long user IDs", async () => {
const longUserId = "user-" + "x".repeat(1000);
const token = await createTestJWT(longUserId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const userId = await getUserID(event);
expect(userId).toBe(longUserId);
});
it("should handle user ID with unicode characters", async () => {
const unicodeUserId = "user-with-unicode-🔐";
const token = await createTestJWT(unicodeUserId);
const event = createMockEvent({
cookies: { userIDToken: token }
});
const userId = await getUserID(event);
expect(userId).toBe(unicodeUserId);
});
it("should handle admin ID case sensitivity", async () => {
const adminId = env.ADMIN_ID;
const wrongCaseId = adminId.toUpperCase();
// Exact match required
const correctToken = await createTestJWT(adminId);
const wrongCaseToken = await createTestJWT(wrongCaseId);
const correctEvent = createMockEvent({
cookies: { userIDToken: correctToken }
});
const wrongCaseEvent = createMockEvent({
cookies: { userIDToken: wrongCaseToken }
});
const correctPrivilege = await getPrivilegeLevel(correctEvent);
const wrongCasePrivilege = await getPrivilegeLevel(wrongCaseEvent);
expect(correctPrivilege).toBe("admin");
// Wrong case should not get admin access (unless IDs match)
if (adminId !== wrongCaseId) {
expect(wrongCasePrivilege).toBe("user");
}
});
});
describe("Authorization Attack Scenarios", () => {
it("should prevent session fixation attacks", async () => {
// Attacker cannot predict or fix session tokens
const token1 = await createTestJWT("user-1");
const token2 = await createTestJWT("user-1");
// Tokens should be different even for same user
// (Due to different timestamps, though payload is same)
expect(token1).toBeDefined();
expect(token2).toBeDefined();
});
it("should prevent parameter pollution attacks", async () => {
// Multiple cookie values should not cause confusion
const token1 = await createTestJWT("user-1");
const token2 = await createTestJWT("user-2");
// Only first cookie should be used
const event = createMockEvent({
cookies: {
userIDToken: token1
// In practice, duplicate cookies are handled by the framework
}
});
const userId = await getUserID(event);
expect(userId).toBe("user-1");
});
it("should prevent token substitution attacks", async () => {
const legitimateToken = await createTestJWT("victim-user");
const attackerToken = await createTestJWT("attacker-user");
// Each token should only authenticate its respective user
const legitimateEvent = createMockEvent({
cookies: { userIDToken: legitimateToken }
});
const attackerEvent = createMockEvent({
cookies: { userIDToken: attackerToken }
});
const legitimateId = await getUserID(legitimateEvent);
const attackerId = await getUserID(attackerEvent);
expect(legitimateId).toBe("victim-user");
expect(attackerId).toBe("attacker-user");
expect(legitimateId).not.toBe(attackerId);
});
it("should prevent authorization bypass through empty checks", async () => {
const emptyChecks = [null, undefined, "", " ", "null", "undefined"];
for (const check of emptyChecks) {
const event = createMockEvent({
cookies: { userIDToken: check as string }
});
const privilege = await getPrivilegeLevel(event);
expect(privilege).toBe("anonymous");
}
});
});
describe("Multi-User Scenarios", () => {
it("should handle multiple concurrent user sessions", async () => {
const users = ["user-1", "user-2", "user-3", "user-4", "user-5"];
const tokens = await Promise.all(users.map((u) => createTestJWT(u)));
const events = tokens.map((token) =>
createMockEvent({ cookies: { userIDToken: token } })
);
const userIds = await Promise.all(events.map(getUserID));
// All users should be correctly identified
expect(userIds).toEqual(users);
});
it("should maintain separate privileges for different users", async () => {
const adminToken = await createTestJWT(env.ADMIN_ID);
const user1Token = await createTestJWT("user-1");
const user2Token = await createTestJWT("user-2");
const adminEvent = createMockEvent({
cookies: { userIDToken: adminToken }
});
const user1Event = createMockEvent({
cookies: { userIDToken: user1Token }
});
const user2Event = createMockEvent({
cookies: { userIDToken: user2Token }
});
const [adminPriv, user1Priv, user2Priv] = await Promise.all([
getPrivilegeLevel(adminEvent),
getPrivilegeLevel(user1Event),
getPrivilegeLevel(user2Event)
]);
expect(adminPriv).toBe("admin");
expect(user1Priv).toBe("user");
expect(user2Priv).toBe("user");
});
});
describe("Performance", () => {
it("should check privileges efficiently", async () => {
const userToken = await createTestJWT("perf-test-user");
const event = createMockEvent({
cookies: { userIDToken: userToken }
});
const start = performance.now();
for (let i = 0; i < 1000; i++) {
await getPrivilegeLevel(event);
}
const duration = performance.now() - start;
// Should complete 1000 checks in less than 100ms
expect(duration).toBeLessThan(100);
});
it("should extract user IDs efficiently", async () => {
const userToken = await createTestJWT("perf-test-user");
const event = createMockEvent({
cookies: { userIDToken: userToken }
});
const start = performance.now();
for (let i = 0; i < 1000; i++) {
await getUserID(event);
}
const duration = performance.now() - start;
// Should complete 1000 extractions in less than 100ms
expect(duration).toBeLessThan(100);
});
});
});

View File

@@ -0,0 +1,320 @@
/**
* CSRF Protection Tests
* Tests for Cross-Site Request Forgery protection mechanisms
*/
import { describe, it, expect, beforeEach } from "bun:test";
import {
generateCSRFToken,
setCSRFToken,
validateCSRFToken,
csrfProtection
} from "~/server/security";
import { createMockEvent } from "./test-utils";
describe("CSRF Protection", () => {
describe("generateCSRFToken", () => {
it("should generate a valid UUID token", () => {
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
);
});
it("should generate unique tokens", () => {
const token1 = generateCSRFToken();
const token2 = generateCSRFToken();
expect(token1).not.toBe(token2);
});
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());
}
expect(tokens.size).toBe(1000);
});
});
describe("setCSRFToken", () => {
it("should set CSRF token cookie with correct attributes", () => {
const event = createMockEvent({});
const token = setCSRFToken(event);
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
);
});
it("should generate different tokens on subsequent calls", () => {
const event1 = createMockEvent({});
const event2 = createMockEvent({});
const token1 = setCSRFToken(event1);
const token2 = setCSRFToken(event2);
expect(token1).not.toBe(token2);
});
});
describe("validateCSRFToken", () => {
it("should validate matching tokens", () => {
const token = generateCSRFToken();
const event = createMockEvent({
headers: { "x-csrf-token": token },
cookies: { "csrf-token": token }
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(true);
});
it("should reject mismatched tokens", () => {
const event = createMockEvent({
headers: { "x-csrf-token": "token1" },
cookies: { "csrf-token": "token2" }
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(false);
});
it("should reject missing header token", () => {
const event = createMockEvent({
cookies: { "csrf-token": "token" }
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(false);
});
it("should reject missing cookie token", () => {
const event = createMockEvent({
headers: { "x-csrf-token": "token" }
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(false);
});
it("should reject empty tokens", () => {
const event = createMockEvent({
headers: { "x-csrf-token": "" },
cookies: { "csrf-token": "" }
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(false);
});
it("should use constant-time comparison", async () => {
const validToken = "a".repeat(36);
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 }
});
const start1 = performance.now();
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 }
});
const start2 = performance.now();
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);
});
it("should reject tokens with different lengths", () => {
const event = createMockEvent({
headers: { "x-csrf-token": "short" },
cookies: { "csrf-token": "much-longer-token" }
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(false);
});
});
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" }
});
const isValid = validateCSRFToken(attackEvent);
expect(isValid).toBe(false);
});
it("should prevent token reuse from different session", () => {
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 }
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(false);
});
it("should prevent token modification", () => {
const token = generateCSRFToken();
const modifiedToken = token.slice(0, -1) + "x";
const event = createMockEvent({
headers: { "x-csrf-token": modifiedToken },
cookies: { "csrf-token": token }
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(false);
});
it("should prevent replay attacks with old tokens", () => {
// Simulate an old token that was captured
const oldToken = "old-captured-token-12345";
const event = createMockEvent({
headers: { "x-csrf-token": oldToken },
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
});
});
describe("Edge Cases", () => {
it("should handle null tokens", () => {
const event = createMockEvent({});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(false);
});
it("should handle undefined tokens", () => {
const event = createMockEvent({
headers: {},
cookies: {}
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(false);
});
it("should handle special characters in tokens", () => {
const token = "token-with-special-!@#$%^&*()";
const event = createMockEvent({
headers: { "x-csrf-token": token },
cookies: { "csrf-token": token }
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(true);
});
it("should handle very long tokens", () => {
const longToken = "a".repeat(1000);
const event = createMockEvent({
headers: { "x-csrf-token": longToken },
cookies: { "csrf-token": longToken }
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(true);
});
it("should handle unicode tokens", () => {
const unicodeToken = "token-with-unicode-🔒🛡️";
const event = createMockEvent({
headers: { "x-csrf-token": unicodeToken },
cookies: { "csrf-token": unicodeToken }
});
const isValid = validateCSRFToken(event);
expect(isValid).toBe(true);
});
});
describe("Token Generation Security", () => {
it("should not generate predictable tokens", () => {
const tokens: string[] = [];
for (let i = 0; i < 100; i++) {
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)
);
}
});
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);
});
});
describe("Performance", () => {
it("should generate tokens quickly", () => {
const start = performance.now();
for (let i = 0; i < 1000; i++) {
generateCSRFToken();
}
const duration = performance.now() - start;
// Should generate 1000 tokens in less than 100ms
expect(duration).toBeLessThan(100);
});
it("should validate tokens quickly", () => {
const token = generateCSRFToken();
const event = createMockEvent({
headers: { "x-csrf-token": token },
cookies: { "csrf-token": token }
});
const start = performance.now();
for (let i = 0; i < 10000; i++) {
validateCSRFToken(event);
}
const duration = performance.now() - start;
// Should validate 10000 tokens in less than 100ms
expect(duration).toBeLessThan(100);
});
});
});

View File

@@ -0,0 +1,522 @@
/**
* Input Validation and Injection Tests
* Tests for SQL injection, XSS, and other injection attack prevention
*/
import { describe, it, expect } from "bun:test";
import {
isValidEmail,
validatePassword,
isValidDisplayName
} from "~/lib/validation";
import { SQL_INJECTION_PAYLOADS, XSS_PAYLOADS } from "./test-utils";
import { ConnectionFactory } from "~/server/database";
describe("Input Validation and Injection Prevention", () => {
describe("Email Validation", () => {
it("should accept valid email addresses", () => {
const validEmails = [
"user@example.com",
"test.user@example.com",
"user+tag@example.co.uk",
"user123@test-domain.com",
"first.last@subdomain.example.com"
];
for (const email of validEmails) {
expect(isValidEmail(email)).toBe(true);
}
});
it("should reject invalid email addresses", () => {
const invalidEmails = [
"not-an-email",
"@example.com",
"user@",
"user @example.com",
"user@example",
"user..name@example.com",
"user@.com",
"",
" ",
"user@domain@domain.com"
];
for (const email of invalidEmails) {
expect(isValidEmail(email)).toBe(false);
}
});
it("should reject SQL injection attempts in emails", () => {
const sqlEmails = [
"admin'--@example.com",
"user@example.com'; DROP TABLE User--",
"' OR '1'='1@example.com",
"user@example.com' UNION SELECT",
"admin@example.com'--"
];
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");
}
});
it("should handle very long email addresses", () => {
const longEmail = "a".repeat(1000) + "@example.com";
const result = isValidEmail(longEmail);
// Should handle gracefully
expect(typeof result).toBe("boolean");
});
it("should handle email with unicode characters", () => {
const unicodeEmail = "üser@exämple.com";
const result = isValidEmail(unicodeEmail);
expect(typeof result).toBe("boolean");
});
});
describe("Display Name Validation", () => {
it("should accept valid display names", () => {
const validNames = [
"John Doe",
"Alice",
"Bob Smith Jr.",
"李明",
"José García",
"123User"
];
for (const name of validNames) {
expect(isValidDisplayName(name)).toBe(true);
}
});
it("should reject empty display names", () => {
const invalidNames = ["", " ", "\t", "\n"];
for (const name of invalidNames) {
expect(isValidDisplayName(name)).toBe(false);
}
});
it("should reject excessively long display names", () => {
const longName = "a".repeat(51);
expect(isValidDisplayName(longName)).toBe(false);
});
it("should handle display names with special characters", () => {
const specialNames = [
"User<script>",
"User'--",
'User"OR"1"="1',
"User & Co",
"User@123"
];
for (const name of specialNames) {
const isValid = isValidDisplayName(name);
// Should either accept and sanitize, or reject
expect(typeof isValid).toBe("boolean");
}
});
});
describe("SQL 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 {
// This query uses parameterized args (safe)
const result = await conn.execute({
sql: "SELECT * FROM User WHERE email = ?",
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
expect(error).toBeDefined();
}
});
it("should prevent SQL injection in all query parameters", async () => {
const conn = ConnectionFactory();
for (const payload of SQL_INJECTION_PAYLOADS) {
try {
// Test various injection points
await conn.execute({
sql: "SELECT * FROM User WHERE email = ?",
args: [payload]
});
await conn.execute({
sql: "SELECT * FROM User WHERE display_name = ?",
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
expect(error.message).not.toContain("syntax error");
expect(error.message).not.toContain("SQL");
}
}
});
it("should prevent UNION-based SQL injection", async () => {
const conn = ConnectionFactory();
const unionPayload = "' UNION SELECT password_hash FROM User--";
try {
const result = await conn.execute({
sql: "SELECT email FROM User WHERE email = ?",
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");
}
}
} catch (error) {
// Error is acceptable, SQL injection is not
expect(error).toBeDefined();
}
});
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();
try {
await conn.execute({
sql: "SELECT * FROM User WHERE email = ?",
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 {
// Insert with parameterized query (safe)
await conn.execute({
sql: "INSERT INTO User (id, email, display_name, provider) VALUES (?, ?, ?, ?)",
args: [
"test-user-sqli",
"test-sqli@example.com",
maliciousName,
"email"
]
});
// 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"]
});
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();
}
});
});
describe("XSS 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");
// In practice, these should be sanitized before rendering
// or stored as-is and sanitized on output
}
});
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");
});
it("should handle event handler attributes", () => {
const eventHandlers = [
"onclick=alert('XSS')",
"onerror=alert('XSS')",
"onload=alert('XSS')",
"onfocus=alert('XSS')"
];
for (const handler of eventHandlers) {
const result = isValidDisplayName(handler);
expect(typeof result).toBe("boolean");
}
});
it("should handle javascript: protocol", () => {
const jsProtocol = "javascript:alert('XSS')";
const displayNameValid = isValidDisplayName(jsProtocol);
const emailValid = isValidEmail(jsProtocol);
expect(typeof displayNameValid).toBe("boolean");
expect(typeof emailValid).toBe("boolean");
});
});
describe("Command Injection Prevention", () => {
it("should not execute shell commands from user input", () => {
const commandPayloads = [
"; ls -la",
"| cat /etc/passwd",
"&& rm -rf /",
"`whoami`",
"$(whoami)",
"; DROP TABLE User;--"
];
for (const payload of commandPayloads) {
// These should be treated as strings, not executed
const emailValid = isValidEmail(payload);
const nameValid = isValidDisplayName(payload);
expect(typeof emailValid).toBe("boolean");
expect(typeof nameValid).toBe("boolean");
}
});
});
describe("Path Traversal Prevention", () => {
it("should not allow directory traversal in inputs", () => {
const traversalPayloads = [
"../../../etc/passwd",
"..\\..\\..\\windows\\system32",
"....//....//....//etc/passwd",
"%2e%2e%2f",
"..;/..;/"
];
for (const payload of traversalPayloads) {
const emailValid = isValidEmail(payload);
const nameValid = isValidDisplayName(payload);
expect(typeof emailValid).toBe("boolean");
expect(typeof nameValid).toBe("boolean");
}
});
});
describe("LDAP Injection Prevention", () => {
it("should handle LDAP injection patterns", () => {
const ldapPayloads = [
"*)(uid=*))(|(uid=*",
"admin*",
"*)(&(password=*))",
"*))%00"
];
for (const payload of ldapPayloads) {
const emailValid = isValidEmail(payload);
const nameValid = isValidDisplayName(payload);
expect(typeof emailValid).toBe("boolean");
expect(typeof nameValid).toBe("boolean");
}
});
});
describe("XML Injection Prevention", () => {
it("should handle XML special characters", () => {
const xmlPayloads = [
"<![CDATA[attack]]>",
'<?xml version="1.0"?>',
'<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>',
"&lt;script&gt;alert('XSS')&lt;/script&gt;"
];
for (const payload of xmlPayloads) {
const emailValid = isValidEmail(payload);
const nameValid = isValidDisplayName(payload);
expect(typeof emailValid).toBe("boolean");
expect(typeof nameValid).toBe("boolean");
}
});
});
describe("NoSQL Injection Prevention", () => {
it("should handle MongoDB-style injection attempts", () => {
const nosqlPayloads = [
'{"$gt": ""}',
'{"$ne": null}',
'{"$regex": ".*"}',
'{"$where": "sleep(1000)"}',
'{"username": {"$gt": ""}}'
];
for (const payload of nosqlPayloads) {
const emailValid = isValidEmail(payload);
const nameValid = isValidDisplayName(payload);
expect(typeof emailValid).toBe("boolean");
expect(typeof nameValid).toBe("boolean");
}
});
});
describe("Header Injection Prevention", () => {
it("should reject inputs with newline characters", () => {
const headerInjection = [
"user@example.com\r\nBcc: attacker@evil.com",
"test\nSet-Cookie: admin=true",
"user\r\nLocation: http://evil.com"
];
for (const payload of headerInjection) {
const emailValid = isValidEmail(payload);
expect(emailValid).toBe(false);
}
});
});
describe("Null Byte Injection Prevention", () => {
it("should handle null bytes in input", () => {
const nullBytePayloads = [
"admin\x00.jpg",
"user@example.com\x00admin",
"test\0injection"
];
for (const payload of nullBytePayloads) {
const emailValid = isValidEmail(payload);
const nameValid = isValidDisplayName(payload);
expect(typeof emailValid).toBe("boolean");
expect(typeof nameValid).toBe("boolean");
}
});
});
describe("Edge Cases", () => {
it("should handle extremely long inputs", () => {
const longInput = "a".repeat(100000);
const start = performance.now();
const emailValid = isValidEmail(longInput);
const nameValid = isValidDisplayName(longInput);
const duration = performance.now() - start;
expect(typeof emailValid).toBe("boolean");
expect(typeof nameValid).toBe("boolean");
// Should complete quickly (no ReDoS)
expect(duration).toBeLessThan(100);
});
it("should handle repeated characters", () => {
const repeated = "a".repeat(10000) + "@example.com";
const emailValid = isValidEmail(repeated);
expect(typeof emailValid).toBe("boolean");
});
it("should handle mixed encoding", () => {
const mixedEncoding = "test%40example.com";
const emailValid = isValidEmail(mixedEncoding);
expect(typeof emailValid).toBe("boolean");
});
it("should handle unicode normalization issues", () => {
const unicodePayloads = [
"admin\u0041", // 'A' in unicode
"test\u200B@example.com", // zero-width space
"user\uFEFF@example.com" // zero-width no-break space
];
for (const payload of unicodePayloads) {
const emailValid = isValidEmail(payload);
expect(typeof emailValid).toBe("boolean");
}
});
});
describe("Performance", () => {
it("should validate emails efficiently", () => {
const email = "test@example.com";
const start = performance.now();
for (let i = 0; i < 10000; i++) {
isValidEmail(email);
}
const duration = performance.now() - start;
expect(duration).toBeLessThan(100);
});
it("should validate display names efficiently", () => {
const name = "Test User";
const start = performance.now();
for (let i = 0; i < 10000; i++) {
isValidDisplayName(name);
}
const duration = performance.now() - start;
expect(duration).toBeLessThan(100);
});
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);
});
});
});

View File

@@ -0,0 +1,529 @@
/**
* Password Security Tests
* Tests for password hashing, validation, strength requirements, and timing attacks
*/
import { describe, it, expect } from "bun:test";
import {
hashPassword,
checkPassword,
checkPasswordSafe
} from "~/server/password";
import { validatePassword, passwordsMatch } from "~/lib/validation";
import { measureTime } from "./test-utils";
describe("Password Security", () => {
describe("Password Hashing", () => {
it("should hash passwords using bcrypt", async () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
expect(hash).toBeDefined();
expect(typeof hash).toBe("string");
// Bcrypt hashes start with $2b$ or $2a$
expect(hash).toMatch(/^\$2[ab]\$/);
});
it("should generate unique hashes for same password", async () => {
const password = "TestPassword123!";
const hash1 = await hashPassword(password);
const hash2 = await hashPassword(password);
expect(hash1).not.toBe(hash2);
});
it("should generate hashes with sufficient length", async () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// Bcrypt hashes are 60 characters long
expect(hash.length).toBe(60);
});
it("should handle very long passwords", async () => {
const longPassword = "a".repeat(1000);
const hash = await hashPassword(longPassword);
expect(hash).toBeDefined();
expect(hash.length).toBe(60);
});
it("should handle passwords with special characters", async () => {
const specialPassword = "P@ssw0rd!#$%^&*()_+-=[]{}|;:',.<>?/~`";
const hash = await hashPassword(specialPassword);
expect(hash).toBeDefined();
const match = await checkPassword(specialPassword, hash);
expect(match).toBe(true);
});
it("should handle passwords with unicode characters", async () => {
const unicodePassword = "Pässwörd123🔐🛡";
const hash = await hashPassword(unicodePassword);
expect(hash).toBeDefined();
const match = await checkPassword(unicodePassword, hash);
expect(match).toBe(true);
});
it("should handle empty passwords", async () => {
const emptyPassword = "";
const hash = await hashPassword(emptyPassword);
expect(hash).toBeDefined();
expect(hash.length).toBe(60);
});
});
describe("Password Verification", () => {
it("should verify correct password", async () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
const match = await checkPassword(password, hash);
expect(match).toBe(true);
});
it("should reject incorrect password", async () => {
const password = "TestPassword123!";
const wrongPassword = "WrongPassword123!";
const hash = await hashPassword(password);
const match = await checkPassword(wrongPassword, hash);
expect(match).toBe(false);
});
it("should be case-sensitive", async () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
const match = await checkPassword("testpassword123!", hash);
expect(match).toBe(false);
});
it("should detect single character differences", async () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
const almostMatch = "TestPassword124!";
const match = await checkPassword(almostMatch, hash);
expect(match).toBe(false);
});
it("should reject password with extra characters", async () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
const match = await checkPassword(password + "x", hash);
expect(match).toBe(false);
});
it("should reject password missing characters", async () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
const match = await checkPassword(password.slice(0, -1), hash);
expect(match).toBe(false);
});
});
describe("Timing Attack Prevention", () => {
it("should use constant time comparison in checkPasswordSafe", async () => {
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)
);
expect(result1).toBe(false);
expect(result2).toBe(false);
// Should take similar time
const timingDifference = Math.abs(duration1 - duration2);
expect(timingDifference).toBeLessThan(50);
});
it("should run bcrypt even when user doesn't exist", async () => {
const password = "TestPassword123!";
// checkPasswordSafe should always run bcrypt to prevent timing attacks
const { duration } = await measureTime(() =>
checkPasswordSafe(password, null)
);
// Should take at least a few milliseconds (bcrypt is slow)
expect(duration).toBeGreaterThan(1);
});
it("should have consistent timing for user exists vs not exists", async () => {
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)
);
// Timing should be similar to prevent user enumeration
const timingDifference = Math.abs(existsDuration - notExistsDuration);
expect(timingDifference).toBeLessThan(50);
});
});
describe("Password Validation", () => {
it("should accept strong passwords", () => {
const strongPassword = "MyStr0ng!P@ssw0rd";
const result = validatePassword(strongPassword);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
expect(result.strength).toBe("good");
});
it("should reject passwords shorter than 12 characters", () => {
const shortPassword = "Short1!";
const result = validatePassword(shortPassword);
expect(result.isValid).toBe(false);
expect(result.errors).toContain(
"Password must be at least 12 characters long"
);
});
it("should reject passwords without uppercase letters", () => {
const noUppercase = "lowercase123!@#";
const result = validatePassword(noUppercase);
expect(result.isValid).toBe(false);
expect(result.errors).toContain(
"Password must contain at least one uppercase letter"
);
});
it("should reject passwords without lowercase letters", () => {
const noLowercase = "UPPERCASE123!@#";
const result = validatePassword(noLowercase);
expect(result.isValid).toBe(false);
expect(result.errors).toContain(
"Password must contain at least one lowercase letter"
);
});
it("should reject passwords without numbers", () => {
const noNumbers = "NoNumbersHere!@#";
const result = validatePassword(noNumbers);
expect(result.isValid).toBe(false);
expect(result.errors).toContain(
"Password must contain at least one number"
);
});
it("should reject passwords without special characters", () => {
const noSpecial = "NoSpecialChars123";
const result = validatePassword(noSpecial);
expect(result.isValid).toBe(false);
expect(result.errors).toContain(
"Password must contain at least one special character"
);
});
it("should reject common weak passwords", () => {
const commonPatterns = [
"Password123!",
"Qwerty123456!",
"Letmein12345!",
"Welcome123!@"
];
for (const password of commonPatterns) {
const result = validatePassword(password);
expect(result.isValid).toBe(false);
expect(result.errors.some((e) => e.includes("common patterns"))).toBe(
true
);
}
});
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
expect(validatePassword(fairPassword).strength).toBe("fair");
expect(validatePassword(goodPassword).strength).toBe("good");
expect(validatePassword(strongPassword).strength).toBe("strong");
});
it("should mark weak passwords appropriately", () => {
const weakPassword = "weak";
const result = validatePassword(weakPassword);
expect(result.strength).toBe("weak");
expect(result.isValid).toBe(false);
});
});
describe("Password Matching", () => {
it("should confirm matching passwords", () => {
const password = "TestPassword123!";
const confirmation = "TestPassword123!";
expect(passwordsMatch(password, confirmation)).toBe(true);
});
it("should reject non-matching passwords", () => {
const password = "TestPassword123!";
const confirmation = "DifferentPassword123!";
expect(passwordsMatch(password, confirmation)).toBe(false);
});
it("should reject empty passwords", () => {
expect(passwordsMatch("", "")).toBe(false);
});
it("should be case-sensitive", () => {
const password = "TestPassword123!";
const confirmation = "testpassword123!";
expect(passwordsMatch(password, confirmation)).toBe(false);
});
it("should detect single character differences", () => {
const password = "TestPassword123!";
const confirmation = "TestPassword124!";
expect(passwordsMatch(password, confirmation)).toBe(false);
});
});
describe("Password Attack Scenarios", () => {
it("should resist brute force attacks with bcrypt slowness", async () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
// Measure time for multiple checks (simulating brute force)
const start = performance.now();
const attempts = 10;
for (let i = 0; i < attempts; i++) {
await checkPassword(`attempt${i}`, hash);
}
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
});
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!",
"Admin123!@#",
"Letmein123!"
];
for (const password of commonPasswords) {
const result = validatePassword(password);
expect(result.isValid).toBe(false);
}
});
it("should resist dictionary attacks", () => {
// Dictionary words that should be caught
const dictionaryBased = ["Sunshine123!", "Princess456!", "Dragon789!@"];
for (const password of dictionaryBased) {
const result = validatePassword(password);
expect(result.isValid).toBe(false);
}
});
});
describe("Edge Cases", () => {
it("should handle very long passwords", async () => {
const longPassword = "A1!a" + "x".repeat(1000); // Very long but valid
const hash = await hashPassword(longPassword);
const match = await checkPassword(longPassword, hash);
expect(match).toBe(true);
});
it("should handle passwords with only whitespace", async () => {
const whitespacePassword = " ";
const result = validatePassword(whitespacePassword);
expect(result.isValid).toBe(false);
});
it("should handle null bytes in passwords", async () => {
const nullBytePassword = "Test\0Password123!";
const hash = await hashPassword(nullBytePassword);
const match = await checkPassword(nullBytePassword, hash);
// Behavior may vary - just ensure no crash
expect(typeof match).toBe("boolean");
});
it("should handle passwords with emoji", () => {
const emojiPassword = "MyP@ssw0rd🔐🛡123";
const result = validatePassword(emojiPassword);
expect(result.isValid).toBe(true);
});
it("should handle passwords with newlines", async () => {
const newlinePassword = "Test\nPassword123!";
const hash = await hashPassword(newlinePassword);
const match = await checkPassword(newlinePassword, hash);
expect(match).toBe(true);
});
it("should handle passwords with tabs", async () => {
const tabPassword = "Test\tPassword123!";
const hash = await hashPassword(tabPassword);
const match = await checkPassword(tabPassword, hash);
expect(match).toBe(true);
});
});
describe("Performance", () => {
it("should hash passwords with appropriate slowness", async () => {
const password = "TestPassword123!";
const start = performance.now();
await hashPassword(password);
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);
});
it("should verify passwords with consistent timing", async () => {
const password = "TestPassword123!";
const hash = await hashPassword(password);
const durations: number[] = [];
for (let i = 0; i < 5; i++) {
const start = performance.now();
await checkPassword(password, hash);
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);
});
it("should validate passwords quickly", () => {
const password = "TestPassword123!";
const start = performance.now();
for (let i = 0; i < 10000; i++) {
validatePassword(password);
}
const duration = performance.now() - start;
// Validation is CPU-bound but should be fast
expect(duration).toBeLessThan(100);
});
});
describe("bcrypt Salt Rounds", () => {
it("should use appropriate salt rounds for security", async () => {
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);
});
it("should generate cryptographically random salts", async () => {
const password = "TestPassword123!";
const hashes = await Promise.all(
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]);
}
});
});
});

View File

@@ -0,0 +1,443 @@
/**
* Rate Limiting Tests
* Tests for rate limiting mechanisms on authentication endpoints
*/
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import {
checkRateLimit,
getClientIP,
rateLimitLogin,
rateLimitPasswordReset,
rateLimitRegistration,
rateLimitEmailVerification,
clearRateLimitStore,
RATE_LIMITS
} from "~/server/security";
import { createMockEvent, randomIP } from "./test-utils";
import { TRPCError } from "@trpc/server";
describe("Rate Limiting", () => {
// Clear rate limit store before each test to ensure isolation
beforeEach(() => {
clearRateLimitStore();
});
describe("checkRateLimit", () => {
it("should allow requests within rate limit", () => {
const identifier = `test-${Date.now()}`;
const maxAttempts = 5;
const windowMs = 60000;
for (let i = 0; i < maxAttempts; i++) {
const remaining = checkRateLimit(identifier, maxAttempts, windowMs);
expect(remaining).toBe(maxAttempts - i - 1);
}
});
it("should block requests exceeding rate limit", () => {
const identifier = `test-${Date.now()}`;
const maxAttempts = 3;
const windowMs = 60000;
// Use up all attempts
for (let i = 0; i < maxAttempts; i++) {
checkRateLimit(identifier, maxAttempts, windowMs);
}
// Next attempt should throw
expect(() => {
checkRateLimit(identifier, maxAttempts, windowMs);
}).toThrow(TRPCError);
});
it("should include remaining time in error message", () => {
const identifier = `test-${Date.now()}`;
const maxAttempts = 2;
const windowMs = 60000;
// Use up all attempts
checkRateLimit(identifier, maxAttempts, windowMs);
checkRateLimit(identifier, maxAttempts, windowMs);
try {
checkRateLimit(identifier, maxAttempts, windowMs);
expect.unreachable("Should have thrown TRPCError");
} catch (error) {
expect(error).toBeInstanceOf(TRPCError);
const trpcError = error as TRPCError;
expect(trpcError.code).toBe("TOO_MANY_REQUESTS");
expect(trpcError.message).toMatch(/Try again in \d+ seconds/);
}
});
it("should reset after time window expires", async () => {
const identifier = `test-${Date.now()}`;
const maxAttempts = 3;
const windowMs = 100; // 100ms window for fast testing
// Use up all attempts
for (let i = 0; i < maxAttempts; i++) {
checkRateLimit(identifier, maxAttempts, windowMs);
}
// Should be blocked
expect(() => {
checkRateLimit(identifier, maxAttempts, windowMs);
}).toThrow(TRPCError);
// Wait for window to expire
await new Promise((resolve) => setTimeout(resolve, 150));
// Should be allowed again
const remaining = checkRateLimit(identifier, maxAttempts, windowMs);
expect(remaining).toBe(maxAttempts - 1);
});
it("should handle concurrent requests correctly", () => {
const identifier = `test-${Date.now()}`;
const maxAttempts = 10;
const windowMs = 60000;
// Simulate concurrent requests
const results: number[] = [];
for (let i = 0; i < maxAttempts; i++) {
results.push(checkRateLimit(identifier, maxAttempts, windowMs));
}
// All should succeed with decreasing remaining counts
expect(results).toEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]);
});
it("should isolate different identifiers", () => {
const maxAttempts = 3;
const windowMs = 60000;
const id1 = `test1-${Date.now()}`;
const id2 = `test2-${Date.now()}`;
// Use up attempts for id1
for (let i = 0; i < maxAttempts; i++) {
checkRateLimit(id1, maxAttempts, windowMs);
}
// id1 should be blocked
expect(() => {
checkRateLimit(id1, maxAttempts, windowMs);
}).toThrow(TRPCError);
// id2 should still work
const remaining = checkRateLimit(id2, maxAttempts, windowMs);
expect(remaining).toBe(maxAttempts - 1);
});
});
describe("getClientIP", () => {
it("should extract IP from x-forwarded-for header", () => {
const event = createMockEvent({
headers: { "x-forwarded-for": "192.168.1.1, 10.0.0.1" }
});
const ip = getClientIP(event);
expect(ip).toBe("192.168.1.1");
});
it("should extract IP from x-real-ip header", () => {
const event = createMockEvent({
headers: { "x-real-ip": "192.168.1.2" }
});
const ip = getClientIP(event);
expect(ip).toBe("192.168.1.2");
});
it("should prefer x-forwarded-for over x-real-ip", () => {
const event = createMockEvent({
headers: {
"x-forwarded-for": "192.168.1.1",
"x-real-ip": "192.168.1.2"
}
});
const ip = getClientIP(event);
expect(ip).toBe("192.168.1.1");
});
it("should return unknown when no IP headers present", () => {
const event = createMockEvent({});
const ip = getClientIP(event);
expect(ip).toBe("unknown");
});
it("should trim whitespace from IP addresses", () => {
const event = createMockEvent({
headers: { "x-forwarded-for": " 192.168.1.1 , 10.0.0.1" }
});
const ip = getClientIP(event);
expect(ip).toBe("192.168.1.1");
});
it("should handle IPv6 addresses", () => {
const event = createMockEvent({
headers: {
"x-forwarded-for": "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
}
});
const ip = getClientIP(event);
expect(ip).toBe("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
});
});
describe("rateLimitLogin", () => {
it("should enforce both IP and email rate limits", () => {
const ip = randomIP();
// Should allow up to LOGIN_IP max attempts (5) with different emails
// Use different emails to avoid hitting email rate limit
for (let i = 0; i < RATE_LIMITS.LOGIN_IP.maxAttempts; i++) {
const email = `test-${Date.now()}-${i}@example.com`;
rateLimitLogin(email, ip);
}
// Next attempt should fail due to IP limit
expect(() => {
const email = `test-${Date.now()}-final@example.com`;
rateLimitLogin(email, ip);
}).toThrow(TRPCError);
});
it("should limit by email independently of IP", () => {
const email = `test-${Date.now()}@example.com`;
// Use different IPs but same email
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
rateLimitLogin(email, randomIP());
}
// Next attempt with different IP should still fail due to email limit
expect(() => {
rateLimitLogin(email, randomIP());
}).toThrow(TRPCError);
});
it("should allow different emails from same IP within IP limit", () => {
const ip = randomIP();
// Use different emails but same IP
for (let i = 0; i < RATE_LIMITS.LOGIN_IP.maxAttempts; i++) {
const email = `test${i}-${Date.now()}@example.com`;
rateLimitLogin(email, ip);
}
// Next attempt should fail due to IP limit
expect(() => {
rateLimitLogin(`new-${Date.now()}@example.com`, ip);
}).toThrow(TRPCError);
});
});
describe("rateLimitPasswordReset", () => {
it("should enforce password reset rate limit", () => {
const ip = randomIP();
// Should allow up to PASSWORD_RESET_IP max attempts (3)
for (let i = 0; i < RATE_LIMITS.PASSWORD_RESET_IP.maxAttempts; i++) {
rateLimitPasswordReset(ip);
}
// Next attempt should fail
expect(() => {
rateLimitPasswordReset(ip);
}).toThrow(TRPCError);
});
it("should isolate password reset limits from login limits", () => {
const ip = randomIP();
const email = `test-${Date.now()}@example.com`;
// Use up password reset limit
for (let i = 0; i < RATE_LIMITS.PASSWORD_RESET_IP.maxAttempts; i++) {
rateLimitPasswordReset(ip);
}
// Should still be able to login (different limit)
rateLimitLogin(email, ip);
});
});
describe("rateLimitRegistration", () => {
it("should enforce registration rate limit", () => {
const ip = randomIP();
// Should allow up to REGISTRATION_IP max attempts (3)
for (let i = 0; i < RATE_LIMITS.REGISTRATION_IP.maxAttempts; i++) {
rateLimitRegistration(ip);
}
// Next attempt should fail
expect(() => {
rateLimitRegistration(ip);
}).toThrow(TRPCError);
});
});
describe("rateLimitEmailVerification", () => {
it("should enforce email verification rate limit", () => {
const ip = randomIP();
// Should allow up to EMAIL_VERIFICATION_IP max attempts (5)
for (let i = 0; i < RATE_LIMITS.EMAIL_VERIFICATION_IP.maxAttempts; i++) {
rateLimitEmailVerification(ip);
}
// Next attempt should fail
expect(() => {
rateLimitEmailVerification(ip);
}).toThrow(TRPCError);
});
});
describe("Rate Limit Attack Scenarios", () => {
it("should prevent brute force login attacks", () => {
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 {
rateLimitLogin(email, attackerIP);
} catch (error) {
if (error instanceof TRPCError) {
blockedAtAttempt = i;
break;
}
}
}
// Should be blocked before 10 attempts
expect(blockedAtAttempt).toBeLessThan(10);
expect(blockedAtAttempt).toBeGreaterThan(0);
});
it("should prevent distributed brute force from multiple IPs", () => {
const email = "victim@example.com";
// Simulate distributed attack from different IPs
let blockedAtAttempt = 0;
for (let i = 0; i < 10; i++) {
try {
rateLimitLogin(email, randomIP());
} catch (error) {
if (error instanceof TRPCError) {
blockedAtAttempt = i;
break;
}
}
}
// Should be blocked at email limit (3 attempts)
expect(blockedAtAttempt).toBeLessThanOrEqual(
RATE_LIMITS.LOGIN_EMAIL.maxAttempts
);
});
it("should prevent account enumeration via registration spam", () => {
const attackerIP = randomIP();
// Try to register many accounts to enumerate valid emails
let blockedAtAttempt = 0;
for (let i = 0; i < 10; i++) {
try {
rateLimitRegistration(attackerIP);
} catch (error) {
if (error instanceof TRPCError) {
blockedAtAttempt = i;
break;
}
}
}
// Should be blocked at registration limit (3 attempts)
expect(blockedAtAttempt).toBe(RATE_LIMITS.REGISTRATION_IP.maxAttempts);
});
it("should prevent password reset spam attacks", () => {
const attackerIP = randomIP();
// Try to spam password resets
let blockedAtAttempt = 0;
for (let i = 0; i < 10; i++) {
try {
rateLimitPasswordReset(attackerIP);
} catch (error) {
if (error instanceof TRPCError) {
blockedAtAttempt = i;
break;
}
}
}
// Should be blocked at password reset limit (3 attempts)
expect(blockedAtAttempt).toBe(RATE_LIMITS.PASSWORD_RESET_IP.maxAttempts);
});
});
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(
60000
);
expect(RATE_LIMITS.REGISTRATION_IP.windowMs).toBeGreaterThanOrEqual(
60000
);
expect(RATE_LIMITS.EMAIL_VERIFICATION_IP.windowMs).toBeGreaterThanOrEqual(
60000
);
});
});
describe("Performance", () => {
it("should handle high volume of rate limit checks efficiently", () => {
const start = performance.now();
// Check 1000 different identifiers
for (let i = 0; i < 1000; i++) {
checkRateLimit(`test-${i}`, 5, 60000);
}
const duration = performance.now() - start;
// Should complete in less than 100ms
expect(duration).toBeLessThan(100);
});
it("should not leak memory with many identifiers", () => {
// Create many rate limit entries
for (let i = 0; i < 10000; i++) {
checkRateLimit(`test-${i}`, 5, 60000);
}
// This test mainly ensures no crashes occur
// Memory cleanup is tested by the cleanup interval in security.ts
expect(true).toBe(true);
});
});
});

View File

@@ -0,0 +1,188 @@
/**
* Security Test Utilities
* Shared helpers for security-related tests
*/
import type { H3Event } from "vinxi/http";
import { SignJWT } from "jose";
import { env } from "~/env/server";
/**
* Create a mock H3Event for testing
* Creates a minimal structure that works with our cookie/header fallback logic
*/
export function createMockEvent(options: {
headers?: Record<string, string>;
cookies?: Record<string, string>;
method?: string;
url?: string;
}): H3Event {
const {
headers = {},
cookies = {},
method = "POST",
url = "http://localhost:3000/"
} = options;
const cookieString = Object.entries(cookies)
.map(([key, value]) => `${key}=${value}`)
.join("; ");
const allHeaders = {
...headers,
...(cookieString ? { cookie: cookieString } : {})
};
// Try to create Headers object, fall back to plain object if headers contain invalid values
let headersObj: Headers | Record<string, string>;
try {
headersObj = new Headers(allHeaders);
} catch (e) {
// If Headers constructor fails (e.g., unicode in headers), use plain object
headersObj = allHeaders;
}
// Create mock event with headers accessible via .headers.get() and .node.req.headers
const mockEvent = {
headers: headersObj,
node: {
req: {
headers: allHeaders
},
res: {
cookies: {}
}
}
} as unknown as H3Event;
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
*/
export const SQL_INJECTION_PAYLOADS = [
"' OR '1'='1",
"'; DROP TABLE User; --",
"admin'--",
"' UNION SELECT * FROM User--",
"1' OR 1=1--",
"' OR 'x'='x",
"1; DELETE FROM User WHERE 1=1--",
"' AND 1=0 UNION ALL SELECT * FROM User--"
];
/**
* Common XSS payloads
*/
export const XSS_PAYLOADS = [
"<script>alert('XSS')</script>",
"<img src=x onerror=alert('XSS')>",
"javascript:alert('XSS')",
"<svg onload=alert('XSS')>",
"<iframe src='javascript:alert(\"XSS\")'></iframe>",
"<body onload=alert('XSS')>",
"<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
*/
export async function measureTime<T>(
fn: () => Promise<T>
): Promise<{ result: T; duration: number }> {
const start = Date.now();
const result = await fn();
const duration = Date.now() - start;
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
*/
export function randomIP(): string {
return Array.from({ length: 4 }, () => Math.floor(Math.random() * 256)).join(
"."
);
}