📖 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
name: "session_id"
value: "abc123"
httpOnly: false (JS can read it!)
secure: false (Sent over HTTP!)
sameSite: "None"
Secure Cookie
name: "session_id"
value: "abc123"
httpOnly: true (JS cannot read it)
secure: true (HTTPS only)
sameSite: "Lax" (Prevents CSRF)

🍪 Testing Cookie Security Flags

Here is how you verify that your session cookie is protected against XSS and CSRF.

typescript
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.

typescript
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.

typescript
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

  1. Automate OWASP Top 10 checks. Focus on XSS, broken access control, and security misconfigurations (headers).
  2. Run security tests in a separate project. Tag them as @security and run them nightly or on every PR.
  3. Use realistic payloads. Use the OWASP XSS Filter Evasion Cheat Sheet for your injection tests.
  4. 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

Beginner
How do you check if a cookie has the HttpOnly flag in Playwright?
Show Answer
I use the 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.
Intermediate
How can you test for reflected XSS using Playwright?
Show Answer
I inject a payload like <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.
Advanced
How do you validate that the backend is sending a Content Security Policy (CSP)?
Show Answer
I capture the main navigation response object from 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'.
Scenario
How would you automate a test for an IDOR (Insecure Direct Object Reference) vulnerability?
Show Answer
I would log in as User A and navigate to a private resource, noting the URL or ID. Then, I would use Playwright's 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

🎯 Hands-On Practice Easy

Inspect Your Cookies

  1. Write a test that logs into a web application.
  2. Call const cookies = await context.cookies().
  3. Use console.log(JSON.stringify(cookies, null, 2)) to print the cookie jar.
  4. Inspect the output. Do the session cookies have httpOnly: true and secure: true?

🚀 Mini Project

🏗️ The Header Security Auditor

  1. Write a test that navigates to 3 different pages of a site (Home, Login, Dashboard).
  2. For each page, capture the main response.
  3. Assert that strict-transport-security (HSTS), x-frame-options, and content-security-policy headers are present on all 3 responses.

📝 Quiz

Test your understanding. Click an option to check your answer.

1. Which Playwright method is used to read cookie attributes?
A) page.cookies()
B) context.cookies()
C) page.evaluate(() => document.cookie)
D) context.storageState()
Answer: B — context.cookies() returns the raw cookie objects including their flags.
2. What does the HttpOnly cookie flag do?
A) Forces the cookie to only be sent over HTTPS
B) Prevents JavaScript from reading the cookie via document.cookie
C) Restricts the cookie to a specific domain
D) Deletes the cookie when the browser closes
Answer: B — HttpOnly protects against XSS attacks stealing session IDs.
3. How do you test for reflected XSS in Playwright?
A) Inject a script payload and check if window variables were modified
B) Check the browser console for errors
C) Scan the HTML source code for <script> tags
D) Use page.route() to block scripts
Answer: A — Inject a payload that sets a global variable if executed, then assert the variable is undefined.
4. How do you read the Content Security Policy (CSP) header?
A) page.csp()
B) response.headers()['content-security-policy']
C) context.securityHeaders()
D) CSP cannot be read by Playwright
Answer: B — Capture the response object from page.goto() and inspect the headers dictionary.
5. Why is testing security over plain HTTP (localhost) problematic?
A) Playwright doesn't support HTTP
B) The Secure flag behaves differently, making tests inaccurate
C) APIs are faster over HTTP
D) XSS doesn't work on localhost
Answer: B — Browsers ignore Secure cookies on localhost, which can mask missing flags.
6. What is an IDOR vulnerability?
A) Injecting malicious scripts into a database
B> Accessing another user's resources by changing an ID in the URL/API
C) Stealing cookies via XSS
D) Bypassing CORS restrictions
Answer: B — Insecure Direct Object Reference allows unauthorized access to data by manipulating IDs.
7. Does Playwright replace dedicated DAST tools like OWASP ZAP?
A) Yes, Playwright does everything ZAP does
B) No, but it excels at testing authenticated flows and specific user journeys
C) Yes, Playwright is a security scanner
D) No, Playwright cannot test security at all
Answer: B — They are complementary. Playwright handles auth state better; ZAP scans broader vulnerability types.
8. Which of the following is a valid XSS test payload?
A) SELECT * FROM users
B) <img src=x onerror="alert(1)">
C) ../../../etc/passwd
D) { "role": "admin" }
Answer: B — This attempts to trigger a JavaScript execution via an image error event.
9. What does the SameSite=Lax cookie flag prevent?
A> Cross-Site Scripting (XSS)
B) Cross-Site Request Forgery (CSRF)
C) SQL Injection
D) Clickjacking
Answer: B — It prevents the browser from sending the cookie with cross-site POST requests.
10. Why should security tests be automated in CI?
A) To make the CI pipeline slower
B) To catch security regressions before they reach production, "shifting left"
C) Because manual testing is illegal
D) To replace penetration testers entirely
Answer: B — Automating catches mistakes (like removing a header) instantly during development.

❓ 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 verify HttpOnly, Secure, and SameSite flags.
  • 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 Secure flags behave correctly.