Security Testing — Automating the Defense
Hackers automate their attacks; you should automate your defenses. Learn how to use Playwright to test for XSS vulnerabilities, validate HttpOnly cookie flags, and enforce Content Security Policy (CSP) headers.
📖 The Story: The Exposed Token
A developer at a fintech startup stored the authentication JWT inside localStorage so it was easy to access for API calls. It worked perfectly. But they forgot to set the HttpOnly flag on the fallback session cookie.
A month later, a third-party analytics script they imported was compromised. The malicious script injected a few lines of JavaScript that read document.cookie and sent it to a hacker's server. Because the session cookie lacked the HttpOnly flag, the hackers instantly hijacked user sessions.
The security team was furious. "Why didn't QA catch this?" The QA team responded, "We test functionality, not security." The CTO immediately mandated that all auth cookies must be tested for HttpOnly and Secure flags in every CI run.
The QA team wrote a 3-line Playwright test using context.cookies(). The next time a developer accidentally removed the HttpOnly flag during a refactor, the CI pipeline blocked the merge. The defense was automated.
Security is not a feature; it's a baseline. If it's not tested, it's vulnerable.
🎯 Why Security Testing?
Prevent XSS
Automatically inject payloads into inputs to ensure the app safely escapes malicious scripts.
Cookie Protection
Verify that session cookies have HttpOnly, Secure, and SameSite flags enabled.
Header Validation
Ensure critical security headers like CSP, HSTS, and X-Frame-Options are present on every response.
Shift Security Left
Catch OWASP Top 10 vulnerabilities at PR time, not during a costly penetration test.
🌍 Real World Example
A developer adds a new search feature. The search bar takes the query string and renders it on the results page: Results for: {query}. If they forgot to sanitize the output, a user could search for <script>stealCookies()</script>, and the browser will execute it. A Playwright test can inject this script tag and assert that it appears as plain text on the screen, rather than executing.
🧒 Explain Like I'm 10
Imagine your house has a front door (your login). You lock it every night. But there's a window right next to the door (a vulnerability). If someone reaches through the window, they can unlock the front door from the inside.
A "security test" is like checking every single window in your house to make sure it's locked, even the tiny one in the basement. We try to break into our own house so that the bad guys can't.
🎓 Professional Explanation
Playwright operates inside the browser, giving it a unique vantage point for security testing. It can read network responses to validate HTTP headers (like CSP), inspect the DOM for reflected XSS payloads, and read the browser's cookie jar via context.cookies() to verify cookie attributes.
While it doesn't replace DAST (Dynamic Application Security Testing) tools like OWASP ZAP, Playwright is exceptional at testing authenticated, stateful user flows where traditional security scanners often get lost.
📊 Secure vs Insecure Cookies
What Playwright Looks For in Cookies
Insecure Cookie
Secure Cookie
🍪 Testing Cookie Security Flags
Here is how you verify that your session cookie is protected against XSS and CSRF.
test('session cookie must have security flags', async ({ page, context }) => { // 1. Perform login to get the cookie await page.goto('/login'); await page.fill('#email', 'user@test.com'); await page.fill('#password', 'password'); await page.click('#login'); // 2. Get all cookies from the context const cookies = await context.cookies(); const sessionCookie = cookies.find(c => c.name === 'session_id'); // 3. Assert the flags are present expect(sessionCookie).toBeDefined(); expect(sessionCookie.httpOnly).toBe(true); expect(sessionCookie.secure).toBe(true); expect(sessionCookie.sameSite).toBe('Lax'); // or 'Strict' });
Why this matters: If httpOnly is false, any running JavaScript (including malicious XSS payloads) can read the cookie. If secure is false, the cookie can be intercepted over unencrypted HTTP.
🦠 Testing for Reflected XSS
To test for XSS, inject a harmless but identifiable payload. Then, check if the browser executed it as HTML or safely escaped it as text.
test('search field should prevent reflected XSS', async ({ page }) => { await page.goto('/search'); const payload = '<img src=x onerror="window.__XSS_TRIGGERED__=true">'; // Inject the payload into the search box await page.getByPlaceholder('Search').fill(payload); await page.getByRole('button', { name: 'Search' }).click(); // Wait for results to render await page.waitForLoadState('networkidle'); // Check if the malicious script executed our global variable const wasXssExecuted = await page.evaluate(() => window.__XSS_TRIGGERED__ === true); // Assert it was NOT executed (the app escaped it correctly) expect(wasXssExecuted).toBe(false); });
How it works: If the app fails to sanitize the input, the browser will render the <img> tag, the src=x will fail, and the onerror handler will execute the JavaScript, setting the global variable. If the variable remains undefined, the app successfully escaped the payload.
🛡️ Validating Content Security Policy (CSP)
CSP headers tell the browser which domains are allowed to execute scripts. You can intercept the main page response and assert that a CSP header exists.
test('page must have a Content Security Policy', async ({ page }) => { const response = await page.goto('/dashboard'); const headers = await response.headers(); // Assert the CSP header exists and restricts script sources expect(headers['content-security-policy']).toContain("script-src 'self'"); });
⚠️ Common Mistakes
Mistake 1: Testing Security over HTTP (localhost)
If you test against http://localhost, the Secure cookie flag won't be enforced by the browser, and your secure: true assertion might fail or behave unpredictably. Always run security tests against a staging environment with HTTPS configured, or use a proxy.
Mistake 2: Relying solely on Playwright for Security
Playwright tests what the user sees. It does not scan backend code for SQL injection or dependency vulnerabilities. It is a piece of the security pie, not the whole pie. Combine it with SAST tools and dependency scanners.
✅ Best Practices
- Automate OWASP Top 10 checks. Focus on XSS, broken access control, and security misconfigurations (headers).
- Run security tests in a separate project. Tag them as
@securityand run them nightly or on every PR. - Use realistic payloads. Use the OWASP XSS Filter Evasion Cheat Sheet for your injection tests.
- Check headers on every page. Don't just check the homepage; verify CSP on API endpoints and static assets too.
🏗️ Senior Engineer Deep Dive
Auth State and Security Testing
Many security vulnerabilities only exist in authenticated states. Because Playwright handles authentication state so well (via storageState), it is vastly superior to traditional DAST scanners for testing authenticated user flows. You can log in once, save the cookie state, and immediately run a suite of XSS and privilege escalation tests as that specific user.
Testing for IDOR (Insecure Direct Object Reference)
You can use Playwright to automate IDOR tests. Log in as User A, extract a resource ID (e.g., /api/invoices/123). Then, use Playwright's API request context to hit that same URL using User B's cookies. If the API returns 200 instead of 403, you have an IDOR vulnerability.
💼 Interview Questions
Show Answer
context.cookies() method, which returns an array of cookie objects. I find the specific cookie by name and assert that its httpOnly property is true.Show Answer
<img src=x onerror="window.xss=true"> into a form field. After submitting, I use page.evaluate(() => window.xss) to check if the payload executed. If it returns true, the app is vulnerable to XSS.Show Answer
page.goto(). I then read the response.headers() object and assert that the 'content-security-policy' header exists and contains the expected directives, like script-src 'self'.Show Answer
request.newContext() with User B's authentication tokens to make a direct API call to that same URL. If the response status is 200 instead of 403 Forbidden, the test fails, indicating an IDOR vulnerability.🏋️ Practical Exercise
Inspect Your Cookies
- Write a test that logs into a web application.
- Call
const cookies = await context.cookies(). - Use
console.log(JSON.stringify(cookies, null, 2))to print the cookie jar. - Inspect the output. Do the session cookies have
httpOnly: trueandsecure: true?
🚀 Mini Project
🏗️ The Header Security Auditor
- Write a test that navigates to 3 different pages of a site (Home, Login, Dashboard).
- For each page, capture the main response.
- Assert that
strict-transport-security(HSTS),x-frame-options, andcontent-security-policyheaders are present on all 3 responses.
📝 Quiz
Test your understanding. Click an option to check your answer.
page.cookies()context.cookies()page.evaluate(() => document.cookie)context.storageState()context.cookies() returns the raw cookie objects including their flags.HttpOnly cookie flag do?document.cookieHttpOnly protects against XSS attacks stealing session IDs.window variables were modified<script> tagspage.route() to block scriptspage.csp()response.headers()['content-security-policy']context.securityHeaders()page.goto() and inspect the headers dictionary.Secure flag behaves differently, making tests inaccurateSecure cookies on localhost, which can mask missing flags.SELECT * FROM users<img src=x onerror="alert(1)">../../../etc/passwd{ "role": "admin" }SameSite=Lax cookie flag prevent?❓ FAQ
Q: Can Playwright test for SQL Injection?
A: Not directly. SQL Injection happens on the backend. However, if an SQL injection causes a visible UI change (like a database error showing on screen), Playwright can detect that. For true SQLi testing, use specialized tools like SQLmap.
Q: How do I test Clickjacking protection?
A: Check the x-frame-options header on the response. If it is set to DENY or SAMEORIGIN, the page cannot be embedded in an iframe by malicious sites, preventing clickjacking.
📦 Summary
🎯 Key Takeaways
- Use
context.cookies()to verifyHttpOnly,Secure, andSameSiteflags. - Test for XSS by injecting payloads and checking if JavaScript executed via
page.evaluate. - Validate security headers like CSP and HSTS using
response.headers(). - Test for IDOR by making API requests as User B to User A's resources.
- Playwright doesn't replace DAST tools, but it is superior for authenticated security testing.
- Always test over HTTPS to ensure
Secureflags behave correctly.
