security hardening
This commit is contained in:
417
src/server/security/authorization.test.ts
Normal file
417
src/server/security/authorization.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user