Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
testing-patterns/security-testing.md
1# Security Testing Basics23## Table of Contents451. [XSS Prevention](#xss-prevention)62. [CSRF Protection](#csrf-protection)73. [Authentication Security](#authentication-security)84. [Authorization Testing](#authorization-testing)95. [Input Validation](#input-validation)106. [Security Headers](#security-headers)1112## XSS Prevention1314### Test Reflected XSS1516```typescript17test("input is properly escaped", async ({ page }) => {18const xssPayloads = [19'<script>alert("xss")</script>',20'<img src="x" onerror="alert(1)">',21'"><script>alert(1)</script>',22"javascript:alert(1)",23'<svg onload="alert(1)">',24];2526for (const payload of xssPayloads) {27await page.goto(`/search?q=${encodeURIComponent(payload)}`);2829// Verify script didn't execute30const alertTriggered = await page.evaluate(() => {31return (window as any).__xssTriggered === true;32});33expect(alertTriggered).toBe(false);3435// Verify payload is escaped in HTML36const content = await page.content();37expect(content).not.toContain("<script>alert");38expect(content).not.toContain("onerror=");39}40});41```4243### Test Stored XSS4445```typescript46test("user content is sanitized", async ({ page }) => {47await page.goto("/create-post");4849// Try to inject script via form50await page.getByLabel("Content").fill('<script>alert("xss")</script>Hello');51await page.getByRole("button", { name: "Submit" }).click();5253// View the post54await page.goto("/posts/latest");5556// Script should not be in page57const scripts = await page.locator("script").count();58const pageContent = await page.content();5960// The script tag should be escaped or removed61expect(pageContent).not.toContain("<script>alert");6263// Text should still be visible (just sanitized)64await expect(page.getByText("Hello")).toBeVisible();65});66```6768### Monitor for XSS Execution6970```typescript71test("no XSS execution", async ({ page }) => {72// Set up XSS detection73await page.addInitScript(() => {74(window as any).__xssDetected = false;7576// Override alert/confirm/prompt77window.alert = () => {78(window as any).__xssDetected = true;79};80window.confirm = () => {81(window as any).__xssDetected = true;82return false;83};84window.prompt = () => {85(window as any).__xssDetected = true;86return null;87};88});8990// Perform test actions91await page.goto("/vulnerable-page");92await page.getByLabel("Search").fill('"><img src=x onerror=alert(1)>');93await page.getByLabel("Search").press("Enter");9495// Check if XSS triggered96const xssDetected = await page.evaluate(() => (window as any).__xssDetected);97expect(xssDetected).toBe(false);98});99```100101## CSRF Protection102103### Verify CSRF Token Present104105```typescript106test("forms include CSRF token", async ({ page }) => {107await page.goto("/settings");108109// Check form has CSRF token110const csrfInput = page.locator(111'input[name="_csrf"], input[name="csrf_token"]',112);113await expect(csrfInput).toBeAttached();114115const csrfValue = await csrfInput.getAttribute("value");116expect(csrfValue).toBeTruthy();117expect(csrfValue!.length).toBeGreaterThan(20);118});119```120121### Test CSRF Token Validation122123```typescript124test("rejects requests without CSRF token", async ({ page, request }) => {125await page.goto("/settings");126127// Try to submit without CSRF token128const response = await request.post("/api/settings", {129data: { theme: "dark" },130headers: {131"Content-Type": "application/json",132},133});134135// Should be rejected136expect(response.status()).toBe(403);137});138139test("rejects requests with invalid CSRF token", async ({ page, request }) => {140await page.goto("/settings");141142const response = await request.post("/api/settings", {143data: { theme: "dark" },144headers: {145"X-CSRF-Token": "invalid-token",146},147});148149expect(response.status()).toBe(403);150});151```152153### Test CSRF with Valid Token154155```typescript156test("accepts requests with valid CSRF token", async ({ page }) => {157await page.goto("/settings");158159// Get CSRF token from page160const csrfToken = await page161.locator('meta[name="csrf-token"]')162.getAttribute("content");163164// Submit form normally165await page.getByLabel("Theme").selectOption("dark");166await page.getByRole("button", { name: "Save" }).click();167168// Should succeed169await expect(page.getByText("Settings saved")).toBeVisible();170});171```172173## Authentication Security174175### Test Session Expiry176177```typescript178test("session expires after timeout", async ({ page, context }) => {179await page.goto("/login");180await page.getByLabel("Email").fill("[email protected]");181await page.getByLabel("Password").fill("password");182await page.getByRole("button", { name: "Sign in" }).click();183184await expect(page).toHaveURL("/dashboard");185186// Simulate time passing (if using clock mocking)187await page.clock.fastForward("02:00:00"); // 2 hours188189// Try to access protected page190await page.goto("/profile");191192// Should redirect to login193await expect(page).toHaveURL(/\/login/);194await expect(page.getByText("Session expired")).toBeVisible();195});196```197198### Test Concurrent Sessions199200```typescript201test("handles concurrent session limit", async ({ browser }) => {202// Login from first browser203const context1 = await browser.newContext();204const page1 = await context1.newPage();205206await page1.goto("/login");207await page1.getByLabel("Email").fill("[email protected]");208await page1.getByLabel("Password").fill("password");209await page1.getByRole("button", { name: "Sign in" }).click();210await expect(page1).toHaveURL("/dashboard");211212// Login from second browser (same user)213const context2 = await browser.newContext();214const page2 = await context2.newPage();215216await page2.goto("/login");217await page2.getByLabel("Email").fill("[email protected]");218await page2.getByLabel("Password").fill("password");219await page2.getByRole("button", { name: "Sign in" }).click();220221// First session should be invalidated (or warning shown)222await page1.reload();223await expect(224page1.getByText(/session.*another device|logged out/i),225).toBeVisible();226227await context1.close();228await context2.close();229});230```231232### Test Password Reset Security233234```typescript235test("password reset token is single-use", async ({ page, request }) => {236// Request password reset237await page.goto("/forgot-password");238await page.getByLabel("Email").fill("[email protected]");239await page.getByRole("button", { name: "Reset" }).click();240241// Get token (in test env, might be exposed or use email mock)242const resetToken = "mock-reset-token";243244// Use token first time245await page.goto(`/reset-password?token=${resetToken}`);246await page.getByLabel("New Password").fill("NewPassword123");247await page.getByRole("button", { name: "Reset" }).click();248249await expect(page.getByText("Password updated")).toBeVisible();250251// Try to use same token again252await page.goto(`/reset-password?token=${resetToken}`);253254await expect(page.getByText("Invalid or expired token")).toBeVisible();255});256```257258## Authorization Testing259260### Test Unauthorized Access261262```typescript263test.describe("authorization", () => {264test("cannot access admin routes as user", async ({ browser }) => {265const context = await browser.newContext({266storageState: ".auth/user.json", // Regular user267});268const page = await context.newPage();269270// Try to access admin page271await page.goto("/admin/users");272273// Should be denied274await expect(page).not.toHaveURL("/admin/users");275expect(276(await page.getByText("Access denied").isVisible()) ||277(await page.url()).includes("/login") ||278(await page.url()).includes("/403"),279).toBe(true);280281await context.close();282});283284test("cannot access other user's data", async ({ page }) => {285// Logged in as user 1, try to access user 2's profile286await page.goto("/users/other-user-id/settings");287288await expect(page.getByText("Access denied")).toBeVisible();289});290});291```292293### Test IDOR (Insecure Direct Object Reference)294295```typescript296test("cannot access other user resources by changing ID", async ({297page,298request,299}) => {300// Get current user's order301await page.goto("/orders/my-order-123");302await expect(page.getByText("Order #my-order-123")).toBeVisible();303304// Try to access another user's order305const response = await request.get("/api/orders/other-user-order-456");306307// Should be forbidden308expect(response.status()).toBe(403);309});310```311312## Input Validation313314### Test SQL Injection Prevention315316```typescript317test("SQL injection is prevented", async ({ page }) => {318const sqlPayloads = [319"'; DROP TABLE users; --",320"1' OR '1'='1",321"1; DELETE FROM orders",322"' UNION SELECT * FROM users --",323];324325for (const payload of sqlPayloads) {326await page.goto("/search");327await page.getByLabel("Search").fill(payload);328await page.getByRole("button", { name: "Search" }).click();329330// Should not error (injection blocked/escaped)331await expect(page.getByText("Error")).not.toBeVisible();332333// Should show no results or escaped text334const hasError = await page335.getByText(/database error|sql|syntax/i)336.isVisible();337expect(hasError).toBe(false);338}339});340```341342### Test Input Length Limits343344```typescript345test("enforces input length limits", async ({ page }) => {346await page.goto("/profile");347348// Try to submit very long input349const longString = "a".repeat(10000);350351await page.getByLabel("Bio").fill(longString);352await page.getByRole("button", { name: "Save" }).click();353354// Should show validation error or truncate355const bioValue = await page.getByLabel("Bio").inputValue();356expect(bioValue.length).toBeLessThanOrEqual(500); // Expected max357});358```359360## Security Headers361362### Verify Security Headers363364```typescript365test("response includes security headers", async ({ page }) => {366const response = await page.goto("/");367368const headers = response!.headers();369370// Content Security Policy371expect(headers["content-security-policy"]).toBeTruthy();372373// Prevent clickjacking374expect(headers["x-frame-options"]).toMatch(/DENY|SAMEORIGIN/);375376// Prevent MIME type sniffing377expect(headers["x-content-type-options"]).toBe("nosniff");378379// XSS Protection (legacy but good to have)380expect(headers["x-xss-protection"]).toBeTruthy();381382// HTTPS enforcement383if (!page.url().includes("localhost")) {384expect(headers["strict-transport-security"]).toBeTruthy();385}386});387```388389### Test CSP Violations390391```typescript392test("CSP blocks inline scripts", async ({ page }) => {393const cspViolations: string[] = [];394395// Listen for CSP violations via console396page.on("console", (msg) => {397if (msg.text().includes("Content Security Policy")) {398cspViolations.push(msg.text());399}400});401402await page.goto("/");403404// Try to inject inline script - CSP should block it405await page.evaluate(() => {406const script = document.createElement("script");407script.textContent = 'console.log("injected")';408document.body.appendChild(script);409});410411expect(cspViolations.length).toBeGreaterThan(0);412});413```414415> **For comprehensive console monitoring** (fixtures, allowed patterns, fail on errors), see [console-errors.md](../debugging/console-errors.md).416417## Anti-Patterns to Avoid418419| Anti-Pattern | Problem | Solution |420| -------------------------- | --------------------- | ----------------------------- |421| Testing only happy path | Misses security holes | Test malicious inputs |422| Hardcoded test credentials | Security risk | Use environment variables |423| Skipping auth tests in dev | Bugs reach production | Test auth in all environments |424| Not testing authorization | Access control bugs | Test all role combinations |425426## Related References427428- **Authentication**: See [fixtures-hooks.md](../core/fixtures-hooks.md) for auth fixtures429- **Multi-User**: See [multi-user.md](../advanced/multi-user.md) for role-based testing430- **Error Testing**: See [error-testing.md](../debugging/error-testing.md) for validation testing431