mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-13 01:32:40 +02:00
12 KiB
12 KiB
Security Testing Basics
Table of Contents
- XSS Prevention
- CSRF Protection
- Authentication Security
- Authorization Testing
- Input Validation
- Security Headers
XSS Prevention
Test Reflected XSS
test("input is properly escaped", async ({ page }) => {
const xssPayloads = [
'<script>alert("xss")</script>',
'<img src="x" onerror="alert(1)">',
'"><script>alert(1)</script>',
"javascript:alert(1)",
'<svg onload="alert(1)">',
];
for (const payload of xssPayloads) {
await page.goto(`/search?q=${encodeURIComponent(payload)}`);
// Verify script didn't execute
const alertTriggered = await page.evaluate(() => {
return (window as any).__xssTriggered === true;
});
expect(alertTriggered).toBe(false);
// Verify payload is escaped in HTML
const content = await page.content();
expect(content).not.toContain("<script>alert");
expect(content).not.toContain("onerror=");
}
});
Test Stored XSS
test("user content is sanitized", async ({ page }) => {
await page.goto("/create-post");
// Try to inject script via form
await page.getByLabel("Content").fill('<script>alert("xss")</script>Hello');
await page.getByRole("button", { name: "Submit" }).click();
// View the post
await page.goto("/posts/latest");
// Script should not be in page
const scripts = await page.locator("script").count();
const pageContent = await page.content();
// The script tag should be escaped or removed
expect(pageContent).not.toContain("<script>alert");
// Text should still be visible (just sanitized)
await expect(page.getByText("Hello")).toBeVisible();
});
Monitor for XSS Execution
test("no XSS execution", async ({ page }) => {
// Set up XSS detection
await page.addInitScript(() => {
(window as any).__xssDetected = false;
// Override alert/confirm/prompt
window.alert = () => {
(window as any).__xssDetected = true;
};
window.confirm = () => {
(window as any).__xssDetected = true;
return false;
};
window.prompt = () => {
(window as any).__xssDetected = true;
return null;
};
});
// Perform test actions
await page.goto("/vulnerable-page");
await page.getByLabel("Search").fill('"><img src=x onerror=alert(1)>');
await page.getByLabel("Search").press("Enter");
// Check if XSS triggered
const xssDetected = await page.evaluate(() => (window as any).__xssDetected);
expect(xssDetected).toBe(false);
});
CSRF Protection
Verify CSRF Token Present
test("forms include CSRF token", async ({ page }) => {
await page.goto("/settings");
// Check form has CSRF token
const csrfInput = page.locator(
'input[name="_csrf"], input[name="csrf_token"]',
);
await expect(csrfInput).toBeAttached();
const csrfValue = await csrfInput.getAttribute("value");
expect(csrfValue).toBeTruthy();
expect(csrfValue!.length).toBeGreaterThan(20);
});
Test CSRF Token Validation
test("rejects requests without CSRF token", async ({ page, request }) => {
await page.goto("/settings");
// Try to submit without CSRF token
const response = await request.post("/api/settings", {
data: { theme: "dark" },
headers: {
"Content-Type": "application/json",
},
});
// Should be rejected
expect(response.status()).toBe(403);
});
test("rejects requests with invalid CSRF token", async ({ page, request }) => {
await page.goto("/settings");
const response = await request.post("/api/settings", {
data: { theme: "dark" },
headers: {
"X-CSRF-Token": "invalid-token",
},
});
expect(response.status()).toBe(403);
});
Test CSRF with Valid Token
test("accepts requests with valid CSRF token", async ({ page }) => {
await page.goto("/settings");
// Get CSRF token from page
const csrfToken = await page
.locator('meta[name="csrf-token"]')
.getAttribute("content");
// Submit form normally
await page.getByLabel("Theme").selectOption("dark");
await page.getByRole("button", { name: "Save" }).click();
// Should succeed
await expect(page.getByText("Settings saved")).toBeVisible();
});
Authentication Security
Test Session Expiry
test("session expires after timeout", async ({ page, context }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("user@example.com");
await page.getByLabel("Password").fill("password");
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page).toHaveURL("/dashboard");
// Simulate time passing (if using clock mocking)
await page.clock.fastForward("02:00:00"); // 2 hours
// Try to access protected page
await page.goto("/profile");
// Should redirect to login
await expect(page).toHaveURL(/\/login/);
await expect(page.getByText("Session expired")).toBeVisible();
});
Test Concurrent Sessions
test("handles concurrent session limit", async ({ browser }) => {
// Login from first browser
const context1 = await browser.newContext();
const page1 = await context1.newPage();
await page1.goto("/login");
await page1.getByLabel("Email").fill("user@example.com");
await page1.getByLabel("Password").fill("password");
await page1.getByRole("button", { name: "Sign in" }).click();
await expect(page1).toHaveURL("/dashboard");
// Login from second browser (same user)
const context2 = await browser.newContext();
const page2 = await context2.newPage();
await page2.goto("/login");
await page2.getByLabel("Email").fill("user@example.com");
await page2.getByLabel("Password").fill("password");
await page2.getByRole("button", { name: "Sign in" }).click();
// First session should be invalidated (or warning shown)
await page1.reload();
await expect(
page1.getByText(/session.*another device|logged out/i),
).toBeVisible();
await context1.close();
await context2.close();
});
Test Password Reset Security
test("password reset token is single-use", async ({ page, request }) => {
// Request password reset
await page.goto("/forgot-password");
await page.getByLabel("Email").fill("user@example.com");
await page.getByRole("button", { name: "Reset" }).click();
// Get token (in test env, might be exposed or use email mock)
const resetToken = "mock-reset-token";
// Use token first time
await page.goto(`/reset-password?token=${resetToken}`);
await page.getByLabel("New Password").fill("NewPassword123");
await page.getByRole("button", { name: "Reset" }).click();
await expect(page.getByText("Password updated")).toBeVisible();
// Try to use same token again
await page.goto(`/reset-password?token=${resetToken}`);
await expect(page.getByText("Invalid or expired token")).toBeVisible();
});
Authorization Testing
Test Unauthorized Access
test.describe("authorization", () => {
test("cannot access admin routes as user", async ({ browser }) => {
const context = await browser.newContext({
storageState: ".auth/user.json", // Regular user
});
const page = await context.newPage();
// Try to access admin page
await page.goto("/admin/users");
// Should be denied
await expect(page).not.toHaveURL("/admin/users");
expect(
(await page.getByText("Access denied").isVisible()) ||
(await page.url()).includes("/login") ||
(await page.url()).includes("/403"),
).toBe(true);
await context.close();
});
test("cannot access other user's data", async ({ page }) => {
// Logged in as user 1, try to access user 2's profile
await page.goto("/users/other-user-id/settings");
await expect(page.getByText("Access denied")).toBeVisible();
});
});
Test IDOR (Insecure Direct Object Reference)
test("cannot access other user resources by changing ID", async ({
page,
request,
}) => {
// Get current user's order
await page.goto("/orders/my-order-123");
await expect(page.getByText("Order #my-order-123")).toBeVisible();
// Try to access another user's order
const response = await request.get("/api/orders/other-user-order-456");
// Should be forbidden
expect(response.status()).toBe(403);
});
Input Validation
Test SQL Injection Prevention
test("SQL injection is prevented", async ({ page }) => {
const sqlPayloads = [
"'; DROP TABLE users; --",
"1' OR '1'='1",
"1; DELETE FROM orders",
"' UNION SELECT * FROM users --",
];
for (const payload of sqlPayloads) {
await page.goto("/search");
await page.getByLabel("Search").fill(payload);
await page.getByRole("button", { name: "Search" }).click();
// Should not error (injection blocked/escaped)
await expect(page.getByText("Error")).not.toBeVisible();
// Should show no results or escaped text
const hasError = await page
.getByText(/database error|sql|syntax/i)
.isVisible();
expect(hasError).toBe(false);
}
});
Test Input Length Limits
test("enforces input length limits", async ({ page }) => {
await page.goto("/profile");
// Try to submit very long input
const longString = "a".repeat(10000);
await page.getByLabel("Bio").fill(longString);
await page.getByRole("button", { name: "Save" }).click();
// Should show validation error or truncate
const bioValue = await page.getByLabel("Bio").inputValue();
expect(bioValue.length).toBeLessThanOrEqual(500); // Expected max
});
Security Headers
Verify Security Headers
test("response includes security headers", async ({ page }) => {
const response = await page.goto("/");
const headers = response!.headers();
// Content Security Policy
expect(headers["content-security-policy"]).toBeTruthy();
// Prevent clickjacking
expect(headers["x-frame-options"]).toMatch(/DENY|SAMEORIGIN/);
// Prevent MIME type sniffing
expect(headers["x-content-type-options"]).toBe("nosniff");
// XSS Protection (legacy but good to have)
expect(headers["x-xss-protection"]).toBeTruthy();
// HTTPS enforcement
if (!page.url().includes("localhost")) {
expect(headers["strict-transport-security"]).toBeTruthy();
}
});
Test CSP Violations
test("CSP blocks inline scripts", async ({ page }) => {
const cspViolations: string[] = [];
// Listen for CSP violations via console
page.on("console", (msg) => {
if (msg.text().includes("Content Security Policy")) {
cspViolations.push(msg.text());
}
});
await page.goto("/");
// Try to inject inline script - CSP should block it
await page.evaluate(() => {
const script = document.createElement("script");
script.textContent = 'console.log("injected")';
document.body.appendChild(script);
});
expect(cspViolations.length).toBeGreaterThan(0);
});
For comprehensive console monitoring (fixtures, allowed patterns, fail on errors), see console-errors.md.
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Testing only happy path | Misses security holes | Test malicious inputs |
| Hardcoded test credentials | Security risk | Use environment variables |
| Skipping auth tests in dev | Bugs reach production | Test auth in all environments |
| Not testing authorization | Access control bugs | Test all role combinations |
Related References
- Authentication: See fixtures-hooks.md for auth fixtures
- Multi-User: See multi-user.md for role-based testing
- Error Testing: See error-testing.md for validation testing