forked from Mike/freno-dev
523 lines
15 KiB
TypeScript
523 lines
15 KiB
TypeScript
/**
|
|
* 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">]>',
|
|
"<script>alert('XSS')</script>"
|
|
];
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|