level 11 / xss-and-csrf
XSS & CSRF
Test for Cross-Site Scripting and Cross-Site Request Forgery vulnerabilities with Playwright.
Cross-Site Scripting (XSS)
XSS injects malicious scripts into web pages viewed by other users. Three types:
| Type | Mechanism |
|---|---|
| Stored | Malicious payload saved in DB, served to all visitors |
| Reflected | Payload in URL parameter, reflected in response |
| DOM-based | Client-side JS reads attacker-controlled data into DOM |
Testing for XSS with Playwright
const XSS_PAYLOADS = [
'<script>alert("xss")</script>',
'"><img src=x onerror=alert(1)>',
"'; alert('xss'); //",
'<svg onload=alert(1)>',
'javascript:alert(1)',
];
test('comment form sanitises XSS payloads', async ({ page }) => {
for (const payload of XSS_PAYLOADS) {
// Submit payload
await page.goto('/comments');
await page.fill('[name=comment]', payload);
await page.click('[type=submit]');
// Detect alert dialog (indicates XSS executed)
let dialogFired = false;
page.once('dialog', async (dialog) => {
dialogFired = true;
await dialog.dismiss();
});
await page.reload();
await page.waitForTimeout(1000);
expect(dialogFired).toBe(false); // no alert = XSS prevented
}
});
Verifying Sanitisation (Not Escaped)
test('XSS payload stored as escaped text, not HTML', async ({ page, request }) => {
const payload = '<script>alert("xss")</script>';
await request.post('/api/comments', {
data: { text: payload },
});
await page.goto('/comments');
// The script tag should appear as visible text, not execute
const commentText = await page.locator('.comment').last().textContent();
expect(commentText).toContain('<script>'); // visible escaped text
expect(commentText).not.toContain('undefined'); // not executed
// Alternatively: check DOM — should be text node, not element
const scriptTags = await page.locator('.comment script').count();
expect(scriptTags).toBe(0);
});
CSP Header Verification
Content Security Policy blocks inline script execution even if XSS payload is injected:
test('Content-Security-Policy header is set', async ({ request }) => {
const res = await request.get('/');
const csp = res.headers()['content-security-policy'];
expect(csp).toBeDefined();
expect(csp).toContain("script-src");
expect(csp).not.toContain("'unsafe-inline'"); // unsafe-inline defeats CSP
});
Cross-Site Request Forgery (CSRF)
CSRF tricks a logged-in user’s browser into making unauthorised requests to another site.
Attack flow:
- User logs into
bank.com(cookie set) - User visits
evil.com evil.comcontains<img src="https://bank.com/transfer?to=attacker&amount=1000">- Browser sends the request with
bank.comcookie — transfer executes
Defences:
- CSRF tokens (synchroniser token pattern)
SameSite=StrictorSameSite=Laxcookies- Origin/Referer header validation
- Custom request headers (AJAX requests send
X-Requested-With)
Testing CSRF Protection
test('state-changing request requires CSRF token', async ({ request }) => {
// Simulate cross-origin request without CSRF token
const res = await request.post('/api/account/change-password', {
headers: {
// No Origin/Referer matching the expected domain
'Origin': 'https://attacker.example.com',
'Content-Type': 'application/json',
},
data: { newPassword: 'hacked123!' },
});
expect([403, 400]).toContain(res.status()); // rejected
});
test('session cookie has SameSite attribute', async ({ page }) => {
await page.goto('/login');
await page.fill('[name=email]', 'test@test.com');
await page.fill('[name=password]', 'password');
await page.click('[type=submit]');
const cookies = await page.context().cookies();
const sessionCookie = cookies.find((c) => c.name === 'session');
expect(sessionCookie).toBeDefined();
expect(['Strict', 'Lax']).toContain(sessionCookie!.sameSite);
});
Security Headers Audit
const REQUIRED_HEADERS = {
'x-content-type-options': 'nosniff',
'x-frame-options': 'DENY',
'referrer-policy': expect.stringContaining('same-origin'),
};
test('security headers are present', async ({ request }) => {
const res = await request.get('/');
const headers = res.headers();
for (const [header, expected] of Object.entries(REQUIRED_HEADERS)) {
if (typeof expected === 'string') {
expect(headers[header]).toBe(expected);
} else {
expect(headers[header]).toEqual(expected);
}
}
});