@assertrx/dino 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- package/themes/dino.json +66 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
# Security Testing
|
|
2
|
+
|
|
3
|
+
> **When to use**: Validating your application's defenses against common web vulnerabilities — XSS, CSRF, insecure cookies, missing headers, authentication bypass, and sensitive data exposure. Playwright is not a replacement for dedicated security scanners, but it catches the most common issues as part of your E2E suite.
|
|
4
|
+
> **Prerequisites**: [core/assertions-and-waiting.md](assertions-and-waiting.md), [core/authentication.md](authentication.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Check security headers on every navigation
|
|
10
|
+
const response = await page.goto('/dashboard');
|
|
11
|
+
expect(response.headers()['content-security-policy']).toBeDefined();
|
|
12
|
+
expect(response.headers()['x-frame-options']).toBe('DENY');
|
|
13
|
+
|
|
14
|
+
// Verify cookie security flags
|
|
15
|
+
const cookies = await context.cookies();
|
|
16
|
+
const sessionCookie = cookies.find(c => c.name === 'session');
|
|
17
|
+
expect(sessionCookie.httpOnly).toBe(true);
|
|
18
|
+
expect(sessionCookie.secure).toBe(true);
|
|
19
|
+
expect(sessionCookie.sameSite).toBe('Strict');
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Patterns
|
|
23
|
+
|
|
24
|
+
### XSS Injection Testing
|
|
25
|
+
|
|
26
|
+
**Use when**: Verifying that user inputs are properly sanitized and rendered as text, not HTML.
|
|
27
|
+
**Avoid when**: You need comprehensive XSS scanning — use a dedicated tool like OWASP ZAP alongside Playwright.
|
|
28
|
+
|
|
29
|
+
**TypeScript**
|
|
30
|
+
```typescript
|
|
31
|
+
import { test, expect } from '@playwright/test';
|
|
32
|
+
|
|
33
|
+
const XSS_PAYLOADS = [
|
|
34
|
+
'<script>alert("xss")</script>',
|
|
35
|
+
'<img src=x onerror=alert("xss")>',
|
|
36
|
+
'"><script>alert("xss")</script>',
|
|
37
|
+
"javascript:alert('xss')",
|
|
38
|
+
'<svg onload=alert("xss")>',
|
|
39
|
+
'{{constructor.constructor("alert(1)")()}}', // Template injection
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
test.describe('XSS protection', () => {
|
|
43
|
+
for (const payload of XSS_PAYLOADS) {
|
|
44
|
+
test(`input sanitizes: ${payload.slice(0, 40)}...`, async ({ page }) => {
|
|
45
|
+
await page.goto('/profile/edit');
|
|
46
|
+
|
|
47
|
+
// Inject the payload into a text field
|
|
48
|
+
await page.getByLabel('Display name').fill(payload);
|
|
49
|
+
await page.getByRole('button', { name: 'Save' }).click();
|
|
50
|
+
|
|
51
|
+
// Verify the payload is rendered as text, not executed
|
|
52
|
+
await page.goto('/profile');
|
|
53
|
+
const displayName = page.getByTestId('display-name');
|
|
54
|
+
await expect(displayName).toBeVisible();
|
|
55
|
+
|
|
56
|
+
// The payload text should appear literally, not as HTML
|
|
57
|
+
const innerHTML = await displayName.innerHTML();
|
|
58
|
+
expect(innerHTML).not.toContain('<script');
|
|
59
|
+
expect(innerHTML).not.toContain('onerror');
|
|
60
|
+
expect(innerHTML).not.toContain('onload');
|
|
61
|
+
|
|
62
|
+
// No dialog should have appeared (script execution)
|
|
63
|
+
// If a dialog fires, the test will fail because it's unhandled
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('XSS via URL parameters is prevented', async ({ page }) => {
|
|
69
|
+
const xssUrl = '/search?q=<script>alert("xss")</script>';
|
|
70
|
+
await page.goto(xssUrl);
|
|
71
|
+
|
|
72
|
+
// The search term should be displayed as text
|
|
73
|
+
const searchInput = page.getByRole('textbox', { name: 'Search' });
|
|
74
|
+
const value = await searchInput.inputValue();
|
|
75
|
+
expect(value).not.toContain('<script');
|
|
76
|
+
|
|
77
|
+
// Page should not have injected script tags
|
|
78
|
+
const scriptCount = await page.locator('script:not([src])').count();
|
|
79
|
+
const pageContent = await page.content();
|
|
80
|
+
expect(pageContent).not.toContain('alert("xss")');
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**JavaScript**
|
|
85
|
+
```javascript
|
|
86
|
+
const { test, expect } = require('@playwright/test');
|
|
87
|
+
|
|
88
|
+
const XSS_PAYLOADS = [
|
|
89
|
+
'<script>alert("xss")</script>',
|
|
90
|
+
'<img src=x onerror=alert("xss")>',
|
|
91
|
+
'"><script>alert("xss")</script>',
|
|
92
|
+
'<svg onload=alert("xss")>',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
test.describe('XSS protection', () => {
|
|
96
|
+
for (const payload of XSS_PAYLOADS) {
|
|
97
|
+
test(`input sanitizes: ${payload.slice(0, 40)}...`, async ({ page }) => {
|
|
98
|
+
await page.goto('/profile/edit');
|
|
99
|
+
await page.getByLabel('Display name').fill(payload);
|
|
100
|
+
await page.getByRole('button', { name: 'Save' }).click();
|
|
101
|
+
|
|
102
|
+
await page.goto('/profile');
|
|
103
|
+
const innerHTML = await page.getByTestId('display-name').innerHTML();
|
|
104
|
+
expect(innerHTML).not.toContain('<script');
|
|
105
|
+
expect(innerHTML).not.toContain('onerror');
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### CSRF Token Verification
|
|
112
|
+
|
|
113
|
+
**Use when**: Ensuring state-changing requests include valid CSRF tokens and the server rejects requests without them.
|
|
114
|
+
**Avoid when**: Your API uses token-based auth (JWT) with no cookie-based sessions — CSRF is not applicable.
|
|
115
|
+
|
|
116
|
+
**TypeScript**
|
|
117
|
+
```typescript
|
|
118
|
+
import { test, expect } from '@playwright/test';
|
|
119
|
+
|
|
120
|
+
test('form submissions include CSRF token', async ({ page }) => {
|
|
121
|
+
await page.goto('/settings');
|
|
122
|
+
|
|
123
|
+
// Verify the CSRF token is present in the form
|
|
124
|
+
const csrfInput = page.locator('input[name="_csrf"], input[name="csrf_token"]');
|
|
125
|
+
await expect(csrfInput).toBeAttached();
|
|
126
|
+
const tokenValue = await csrfInput.inputValue();
|
|
127
|
+
expect(tokenValue).toBeTruthy();
|
|
128
|
+
expect(tokenValue.length).toBeGreaterThan(16);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('server rejects requests without CSRF token', async ({ page, request }) => {
|
|
132
|
+
// First, get a valid session by logging in through the UI
|
|
133
|
+
await page.goto('/login');
|
|
134
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
135
|
+
await page.getByLabel('Password').fill('password123');
|
|
136
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
137
|
+
await page.waitForURL('/dashboard');
|
|
138
|
+
|
|
139
|
+
// Attempt a state-changing request without the CSRF token
|
|
140
|
+
const cookies = await page.context().cookies();
|
|
141
|
+
const response = await request.post('/api/settings', {
|
|
142
|
+
headers: {
|
|
143
|
+
Cookie: cookies.map(c => `${c.name}=${c.value}`).join('; '),
|
|
144
|
+
},
|
|
145
|
+
data: { theme: 'dark' }, // No CSRF token
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Server should reject it
|
|
149
|
+
expect(response.status()).toBe(403);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('CSRF token rotates per session', async ({ browser }) => {
|
|
153
|
+
const context1 = await browser.newContext();
|
|
154
|
+
const context2 = await browser.newContext();
|
|
155
|
+
|
|
156
|
+
const page1 = await context1.newPage();
|
|
157
|
+
const page2 = await context2.newPage();
|
|
158
|
+
|
|
159
|
+
await page1.goto('/login');
|
|
160
|
+
await page2.goto('/login');
|
|
161
|
+
|
|
162
|
+
const token1 = await page1.locator('input[name="_csrf"]').inputValue();
|
|
163
|
+
const token2 = await page2.locator('input[name="_csrf"]').inputValue();
|
|
164
|
+
|
|
165
|
+
// Tokens should differ between sessions
|
|
166
|
+
expect(token1).not.toBe(token2);
|
|
167
|
+
|
|
168
|
+
await context1.close();
|
|
169
|
+
await context2.close();
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**JavaScript**
|
|
174
|
+
```javascript
|
|
175
|
+
const { test, expect } = require('@playwright/test');
|
|
176
|
+
|
|
177
|
+
test('form submissions include CSRF token', async ({ page }) => {
|
|
178
|
+
await page.goto('/settings');
|
|
179
|
+
|
|
180
|
+
const csrfInput = page.locator('input[name="_csrf"], input[name="csrf_token"]');
|
|
181
|
+
await expect(csrfInput).toBeAttached();
|
|
182
|
+
const tokenValue = await csrfInput.inputValue();
|
|
183
|
+
expect(tokenValue).toBeTruthy();
|
|
184
|
+
expect(tokenValue.length).toBeGreaterThan(16);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('server rejects requests without CSRF token', async ({ page, request }) => {
|
|
188
|
+
await page.goto('/login');
|
|
189
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
190
|
+
await page.getByLabel('Password').fill('password123');
|
|
191
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
192
|
+
await page.waitForURL('/dashboard');
|
|
193
|
+
|
|
194
|
+
const cookies = await page.context().cookies();
|
|
195
|
+
const response = await request.post('/api/settings', {
|
|
196
|
+
headers: {
|
|
197
|
+
Cookie: cookies.map(c => `${c.name}=${c.value}`).join('; '),
|
|
198
|
+
},
|
|
199
|
+
data: { theme: 'dark' },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(response.status()).toBe(403);
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### CSP Header Validation
|
|
207
|
+
|
|
208
|
+
**Use when**: Verifying Content Security Policy headers are present and correctly configured.
|
|
209
|
+
**Avoid when**: CSP is managed by infrastructure (CDN/WAF) tested separately.
|
|
210
|
+
|
|
211
|
+
**TypeScript**
|
|
212
|
+
```typescript
|
|
213
|
+
import { test, expect } from '@playwright/test';
|
|
214
|
+
|
|
215
|
+
test('CSP headers are properly configured', async ({ page }) => {
|
|
216
|
+
const response = await page.goto('/');
|
|
217
|
+
const csp = response!.headers()['content-security-policy'];
|
|
218
|
+
|
|
219
|
+
expect(csp).toBeDefined();
|
|
220
|
+
expect(csp).toContain("default-src 'self'");
|
|
221
|
+
expect(csp).not.toContain("'unsafe-inline'"); // Disallow inline scripts
|
|
222
|
+
expect(csp).not.toContain("'unsafe-eval'"); // Disallow eval()
|
|
223
|
+
expect(csp).toContain('script-src');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('security headers are present on all pages', async ({ page }) => {
|
|
227
|
+
const pagesToCheck = ['/', '/login', '/dashboard', '/api/health'];
|
|
228
|
+
|
|
229
|
+
for (const url of pagesToCheck) {
|
|
230
|
+
const response = await page.goto(url);
|
|
231
|
+
const headers = response!.headers();
|
|
232
|
+
|
|
233
|
+
expect(headers['x-content-type-options']).toBe('nosniff');
|
|
234
|
+
expect(headers['x-frame-options']).toMatch(/DENY|SAMEORIGIN/);
|
|
235
|
+
expect(headers['strict-transport-security']).toBeDefined();
|
|
236
|
+
expect(headers['referrer-policy']).toBeDefined();
|
|
237
|
+
expect(headers['x-xss-protection']).toBeUndefined(); // Deprecated, should not be set
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**JavaScript**
|
|
243
|
+
```javascript
|
|
244
|
+
const { test, expect } = require('@playwright/test');
|
|
245
|
+
|
|
246
|
+
test('CSP headers are properly configured', async ({ page }) => {
|
|
247
|
+
const response = await page.goto('/');
|
|
248
|
+
const csp = response.headers()['content-security-policy'];
|
|
249
|
+
|
|
250
|
+
expect(csp).toBeDefined();
|
|
251
|
+
expect(csp).toContain("default-src 'self'");
|
|
252
|
+
expect(csp).not.toContain("'unsafe-inline'");
|
|
253
|
+
expect(csp).not.toContain("'unsafe-eval'");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('security headers are present on all pages', async ({ page }) => {
|
|
257
|
+
const pagesToCheck = ['/', '/login', '/dashboard'];
|
|
258
|
+
|
|
259
|
+
for (const url of pagesToCheck) {
|
|
260
|
+
const response = await page.goto(url);
|
|
261
|
+
const headers = response.headers();
|
|
262
|
+
|
|
263
|
+
expect(headers['x-content-type-options']).toBe('nosniff');
|
|
264
|
+
expect(headers['x-frame-options']).toMatch(/DENY|SAMEORIGIN/);
|
|
265
|
+
expect(headers['strict-transport-security']).toBeDefined();
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Cookie Security Flags
|
|
271
|
+
|
|
272
|
+
**Use when**: Verifying session cookies and auth cookies have proper security attributes.
|
|
273
|
+
**Avoid when**: Your app is fully stateless with no cookies.
|
|
274
|
+
|
|
275
|
+
**TypeScript**
|
|
276
|
+
```typescript
|
|
277
|
+
import { test, expect } from '@playwright/test';
|
|
278
|
+
|
|
279
|
+
test('session cookie has correct security flags', async ({ page, context }) => {
|
|
280
|
+
// Log in to create a session cookie
|
|
281
|
+
await page.goto('/login');
|
|
282
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
283
|
+
await page.getByLabel('Password').fill('password123');
|
|
284
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
285
|
+
await page.waitForURL('/dashboard');
|
|
286
|
+
|
|
287
|
+
const cookies = await context.cookies();
|
|
288
|
+
|
|
289
|
+
// Check session cookie
|
|
290
|
+
const session = cookies.find(c => c.name === 'session' || c.name === 'sid');
|
|
291
|
+
expect(session).toBeDefined();
|
|
292
|
+
expect(session!.httpOnly).toBe(true); // Not accessible via JavaScript
|
|
293
|
+
expect(session!.secure).toBe(true); // Only sent over HTTPS
|
|
294
|
+
expect(session!.sameSite).toBe('Strict'); // Or 'Lax' at minimum
|
|
295
|
+
|
|
296
|
+
// Session cookie should not have an excessive expiry
|
|
297
|
+
if (session!.expires !== -1) {
|
|
298
|
+
const maxAge = session!.expires - Date.now() / 1000;
|
|
299
|
+
expect(maxAge).toBeLessThan(86400 * 30); // No more than 30 days
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('sensitive cookies are not exposed to JavaScript', async ({ page }) => {
|
|
304
|
+
await page.goto('/login');
|
|
305
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
306
|
+
await page.getByLabel('Password').fill('password123');
|
|
307
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
308
|
+
await page.waitForURL('/dashboard');
|
|
309
|
+
|
|
310
|
+
// document.cookie should NOT contain HttpOnly cookies
|
|
311
|
+
const jsCookies = await page.evaluate(() => document.cookie);
|
|
312
|
+
expect(jsCookies).not.toContain('session');
|
|
313
|
+
expect(jsCookies).not.toContain('sid');
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**JavaScript**
|
|
318
|
+
```javascript
|
|
319
|
+
const { test, expect } = require('@playwright/test');
|
|
320
|
+
|
|
321
|
+
test('session cookie has correct security flags', async ({ page, context }) => {
|
|
322
|
+
await page.goto('/login');
|
|
323
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
324
|
+
await page.getByLabel('Password').fill('password123');
|
|
325
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
326
|
+
await page.waitForURL('/dashboard');
|
|
327
|
+
|
|
328
|
+
const cookies = await context.cookies();
|
|
329
|
+
const session = cookies.find(c => c.name === 'session' || c.name === 'sid');
|
|
330
|
+
|
|
331
|
+
expect(session).toBeDefined();
|
|
332
|
+
expect(session.httpOnly).toBe(true);
|
|
333
|
+
expect(session.secure).toBe(true);
|
|
334
|
+
expect(session.sameSite).toBe('Strict');
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Authentication Bypass Testing
|
|
339
|
+
|
|
340
|
+
**Use when**: Ensuring protected routes redirect unauthenticated users and that session invalidation works.
|
|
341
|
+
**Avoid when**: Auth is tested through dedicated API tests that cover these cases already.
|
|
342
|
+
|
|
343
|
+
**TypeScript**
|
|
344
|
+
```typescript
|
|
345
|
+
import { test, expect } from '@playwright/test';
|
|
346
|
+
|
|
347
|
+
test('unauthenticated user cannot access protected routes', async ({ page }) => {
|
|
348
|
+
const protectedRoutes = ['/dashboard', '/settings', '/admin', '/api/users'];
|
|
349
|
+
|
|
350
|
+
for (const route of protectedRoutes) {
|
|
351
|
+
const response = await page.goto(route);
|
|
352
|
+
|
|
353
|
+
// Should redirect to login or return 401/403
|
|
354
|
+
const isRedirected = page.url().includes('/login');
|
|
355
|
+
const isBlocked = response!.status() === 401 || response!.status() === 403;
|
|
356
|
+
expect(isRedirected || isBlocked).toBe(true);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('session is invalidated after logout', async ({ page, context }) => {
|
|
361
|
+
// Log in
|
|
362
|
+
await page.goto('/login');
|
|
363
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
364
|
+
await page.getByLabel('Password').fill('password123');
|
|
365
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
366
|
+
await page.waitForURL('/dashboard');
|
|
367
|
+
|
|
368
|
+
// Capture session cookie
|
|
369
|
+
const cookiesBefore = await context.cookies();
|
|
370
|
+
const sessionBefore = cookiesBefore.find(c => c.name === 'session');
|
|
371
|
+
|
|
372
|
+
// Log out
|
|
373
|
+
await page.getByRole('button', { name: 'Log out' }).click();
|
|
374
|
+
await page.waitForURL('/login');
|
|
375
|
+
|
|
376
|
+
// Verify session cookie is cleared
|
|
377
|
+
const cookiesAfter = await context.cookies();
|
|
378
|
+
const sessionAfter = cookiesAfter.find(c => c.name === 'session');
|
|
379
|
+
expect(sessionAfter).toBeUndefined();
|
|
380
|
+
|
|
381
|
+
// Attempting to access protected route should fail
|
|
382
|
+
await page.goto('/dashboard');
|
|
383
|
+
expect(page.url()).toContain('/login');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('expired session redirects to login', async ({ page, context }) => {
|
|
387
|
+
await page.goto('/login');
|
|
388
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
389
|
+
await page.getByLabel('Password').fill('password123');
|
|
390
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
391
|
+
await page.waitForURL('/dashboard');
|
|
392
|
+
|
|
393
|
+
// Manually expire the session cookie
|
|
394
|
+
await context.clearCookies();
|
|
395
|
+
|
|
396
|
+
// Next navigation should redirect to login
|
|
397
|
+
await page.goto('/dashboard');
|
|
398
|
+
expect(page.url()).toContain('/login');
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**JavaScript**
|
|
403
|
+
```javascript
|
|
404
|
+
const { test, expect } = require('@playwright/test');
|
|
405
|
+
|
|
406
|
+
test('unauthenticated user cannot access protected routes', async ({ page }) => {
|
|
407
|
+
const protectedRoutes = ['/dashboard', '/settings', '/admin'];
|
|
408
|
+
|
|
409
|
+
for (const route of protectedRoutes) {
|
|
410
|
+
await page.goto(route);
|
|
411
|
+
expect(page.url()).toContain('/login');
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('session is invalidated after logout', async ({ page, context }) => {
|
|
416
|
+
await page.goto('/login');
|
|
417
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
418
|
+
await page.getByLabel('Password').fill('password123');
|
|
419
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
420
|
+
await page.waitForURL('/dashboard');
|
|
421
|
+
|
|
422
|
+
await page.getByRole('button', { name: 'Log out' }).click();
|
|
423
|
+
await page.waitForURL('/login');
|
|
424
|
+
|
|
425
|
+
const cookies = await context.cookies();
|
|
426
|
+
const session = cookies.find(c => c.name === 'session');
|
|
427
|
+
expect(session).toBeUndefined();
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### HTTPS Redirect and Sensitive Data Exposure
|
|
432
|
+
|
|
433
|
+
**Use when**: Verifying that HTTP requests are redirected to HTTPS and that sensitive data is not leaked in URLs, headers, or client-side storage.
|
|
434
|
+
**Avoid when**: Running against `localhost` where HTTPS is not configured.
|
|
435
|
+
|
|
436
|
+
**TypeScript**
|
|
437
|
+
```typescript
|
|
438
|
+
import { test, expect } from '@playwright/test';
|
|
439
|
+
|
|
440
|
+
test('HTTP redirects to HTTPS', async ({ request }) => {
|
|
441
|
+
// Use the API request context to follow redirects
|
|
442
|
+
const response = await request.get('http://your-app.com/', {
|
|
443
|
+
maxRedirects: 0, // Don't follow — inspect the redirect
|
|
444
|
+
});
|
|
445
|
+
expect(response.status()).toBe(301);
|
|
446
|
+
expect(response.headers()['location']).toMatch(/^https:\/\//);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('HSTS header is set', async ({ page }) => {
|
|
450
|
+
const response = await page.goto('/');
|
|
451
|
+
const hsts = response!.headers()['strict-transport-security'];
|
|
452
|
+
expect(hsts).toBeDefined();
|
|
453
|
+
expect(hsts).toContain('max-age=');
|
|
454
|
+
|
|
455
|
+
// Extract max-age value and verify it's at least 1 year
|
|
456
|
+
const maxAge = parseInt(hsts!.match(/max-age=(\d+)/)?.[1] || '0');
|
|
457
|
+
expect(maxAge).toBeGreaterThanOrEqual(31536000);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('sensitive data is not in URL parameters', async ({ page }) => {
|
|
461
|
+
await page.goto('/login');
|
|
462
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
463
|
+
await page.getByLabel('Password').fill('password123');
|
|
464
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
465
|
+
await page.waitForURL('/dashboard');
|
|
466
|
+
|
|
467
|
+
// Password should never appear in URL
|
|
468
|
+
expect(page.url()).not.toContain('password');
|
|
469
|
+
expect(page.url()).not.toContain('token');
|
|
470
|
+
expect(page.url()).not.toContain('secret');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('sensitive data is not in localStorage', async ({ page }) => {
|
|
474
|
+
await page.goto('/login');
|
|
475
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
476
|
+
await page.getByLabel('Password').fill('password123');
|
|
477
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
478
|
+
await page.waitForURL('/dashboard');
|
|
479
|
+
|
|
480
|
+
const storageData = await page.evaluate(() => {
|
|
481
|
+
const data: Record<string, string> = {};
|
|
482
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
483
|
+
const key = localStorage.key(i)!;
|
|
484
|
+
data[key] = localStorage.getItem(key)!;
|
|
485
|
+
}
|
|
486
|
+
return JSON.stringify(data);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
expect(storageData).not.toContain('password');
|
|
490
|
+
expect(storageData.toLowerCase()).not.toContain('secret');
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**JavaScript**
|
|
495
|
+
```javascript
|
|
496
|
+
const { test, expect } = require('@playwright/test');
|
|
497
|
+
|
|
498
|
+
test('sensitive data is not in localStorage', async ({ page }) => {
|
|
499
|
+
await page.goto('/login');
|
|
500
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
501
|
+
await page.getByLabel('Password').fill('password123');
|
|
502
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
503
|
+
await page.waitForURL('/dashboard');
|
|
504
|
+
|
|
505
|
+
const storageData = await page.evaluate(() => {
|
|
506
|
+
const data = {};
|
|
507
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
508
|
+
const key = localStorage.key(i);
|
|
509
|
+
data[key] = localStorage.getItem(key);
|
|
510
|
+
}
|
|
511
|
+
return JSON.stringify(data);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
expect(storageData).not.toContain('password');
|
|
515
|
+
expect(storageData.toLowerCase()).not.toContain('secret');
|
|
516
|
+
});
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Session Fixation Prevention
|
|
520
|
+
|
|
521
|
+
**Use when**: Ensuring the session ID changes after authentication to prevent session fixation attacks.
|
|
522
|
+
**Avoid when**: Using stateless token auth (JWT) with no server-side sessions.
|
|
523
|
+
|
|
524
|
+
**TypeScript**
|
|
525
|
+
```typescript
|
|
526
|
+
import { test, expect } from '@playwright/test';
|
|
527
|
+
|
|
528
|
+
test('session ID changes after login', async ({ page, context }) => {
|
|
529
|
+
await page.goto('/login');
|
|
530
|
+
|
|
531
|
+
// Capture pre-login session identifier
|
|
532
|
+
const cookiesBefore = await context.cookies();
|
|
533
|
+
const preLoginSession = cookiesBefore.find(c => c.name === 'session');
|
|
534
|
+
const preLoginValue = preLoginSession?.value;
|
|
535
|
+
|
|
536
|
+
// Log in
|
|
537
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
538
|
+
await page.getByLabel('Password').fill('password123');
|
|
539
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
540
|
+
await page.waitForURL('/dashboard');
|
|
541
|
+
|
|
542
|
+
// Session ID must change after authentication
|
|
543
|
+
const cookiesAfter = await context.cookies();
|
|
544
|
+
const postLoginSession = cookiesAfter.find(c => c.name === 'session');
|
|
545
|
+
expect(postLoginSession).toBeDefined();
|
|
546
|
+
|
|
547
|
+
if (preLoginValue) {
|
|
548
|
+
expect(postLoginSession!.value).not.toBe(preLoginValue);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**JavaScript**
|
|
554
|
+
```javascript
|
|
555
|
+
const { test, expect } = require('@playwright/test');
|
|
556
|
+
|
|
557
|
+
test('session ID changes after login', async ({ page, context }) => {
|
|
558
|
+
await page.goto('/login');
|
|
559
|
+
|
|
560
|
+
const cookiesBefore = await context.cookies();
|
|
561
|
+
const preLoginSession = cookiesBefore.find(c => c.name === 'session');
|
|
562
|
+
const preLoginValue = preLoginSession?.value;
|
|
563
|
+
|
|
564
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
565
|
+
await page.getByLabel('Password').fill('password123');
|
|
566
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
567
|
+
await page.waitForURL('/dashboard');
|
|
568
|
+
|
|
569
|
+
const cookiesAfter = await context.cookies();
|
|
570
|
+
const postLoginSession = cookiesAfter.find(c => c.name === 'session');
|
|
571
|
+
expect(postLoginSession).toBeDefined();
|
|
572
|
+
|
|
573
|
+
if (preLoginValue) {
|
|
574
|
+
expect(postLoginSession.value).not.toBe(preLoginValue);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
## Decision Guide
|
|
580
|
+
|
|
581
|
+
| Vulnerability | Playwright Test Approach | Confidence Level |
|
|
582
|
+
|---|---|---|
|
|
583
|
+
| Reflected XSS | Inject payloads in inputs and URL params, assert no script execution | Medium -- covers common cases, not exhaustive |
|
|
584
|
+
| Stored XSS | Inject payload, reload page, assert sanitized output | Medium -- catches rendering-level issues |
|
|
585
|
+
| CSRF | Verify token presence, test rejection without token | High -- directly tests the mechanism |
|
|
586
|
+
| Insecure cookies | Assert `httpOnly`, `secure`, `sameSite` flags | High -- deterministic check |
|
|
587
|
+
| Missing security headers | Assert header presence and values | High -- deterministic check |
|
|
588
|
+
| Auth bypass | Navigate to protected routes without auth | High -- tests the redirect/block mechanism |
|
|
589
|
+
| Session fixation | Compare session IDs before and after login | High -- directly verifiable |
|
|
590
|
+
| Sensitive data exposure | Check URLs, localStorage, response bodies for secrets | Medium -- catches obvious leaks |
|
|
591
|
+
| HTTPS enforcement | Verify redirect and HSTS header | High -- deterministic check |
|
|
592
|
+
|
|
593
|
+
## Anti-Patterns
|
|
594
|
+
|
|
595
|
+
| Don't Do This | Problem | Do This Instead |
|
|
596
|
+
|---|---|---|
|
|
597
|
+
| Only testing the "happy path" login | Misses bypass vectors | Test unauthenticated access, expired sessions, tampered tokens |
|
|
598
|
+
| Checking `SameSite` only on one cookie | Other cookies may leak session info | Check all cookies that contain session data |
|
|
599
|
+
| Ignoring CSP on API endpoints | APIs can serve HTML on error pages | Check headers on API routes too |
|
|
600
|
+
| Testing security only in development | Dev servers often have relaxed security | Run security tests against staging with production-like config |
|
|
601
|
+
| Using `page.waitForTimeout` after login | Hides timing-based auth issues | Use `page.waitForURL` or assertion-based waiting |
|
|
602
|
+
| Hardcoding test credentials in test files | Credentials leak into version control | Use environment variables or a secrets manager |
|
|
603
|
+
| Skipping HTTPS tests because "it works locally" | HTTP-only local dev hides HTTPS issues | Test HTTPS redirect against staging or use `--ignore-https-errors` carefully |
|
|
604
|
+
| Treating Playwright as a full security scanner | Playwright tests are not penetration tests | Use Playwright for regression checks; pair with OWASP ZAP, Burp Suite, or Snyk for deep scanning |
|
|
605
|
+
|
|
606
|
+
## Troubleshooting
|
|
607
|
+
|
|
608
|
+
| Symptom | Likely Cause | Fix |
|
|
609
|
+
|---|---|---|
|
|
610
|
+
| CSP header missing in test but present in production | Dev server does not set CSP | Run security tests against staging with production config |
|
|
611
|
+
| Cookie `secure` flag is `false` | Testing over HTTP (localhost) | Test against HTTPS staging, or verify the flag is set conditionally for production |
|
|
612
|
+
| CSRF test passes without token | CSRF protection disabled in test environment | Enable CSRF in test environment or run against staging |
|
|
613
|
+
| XSS payload does not execute but test passes | Framework auto-escapes by default | Still test -- the test confirms the protection works; add edge cases for raw HTML rendering |
|
|
614
|
+
| `context.cookies()` returns empty | Cookies set on a different domain or path | Pass the specific URL to `context.cookies('https://your-app.com')` |
|
|
615
|
+
| HSTS header check fails on localhost | HSTS requires HTTPS with valid certs | Skip HSTS tests for localhost, run against staging |
|
|
616
|
+
| Session cookie not found by name | Cookie name differs across environments | Search by pattern: `cookies.find(c => c.name.includes('sess'))` |
|
|
617
|
+
|
|
618
|
+
## Related
|
|
619
|
+
|
|
620
|
+
- [core/authentication.md](authentication.md) -- login flows and auth state management
|
|
621
|
+
- [core/network-mocking.md](network-mocking.md) -- intercepting requests for security testing
|
|
622
|
+
- [core/configuration.md](configuration.md) -- `ignoreHTTPSErrors` and base URL configuration
|
|
623
|
+
- [core/third-party-integrations.md](third-party-integrations.md) -- testing OAuth and third-party auth providers
|
|
624
|
+
- [ci/ci-github-actions.md](../ci/ci-github-actions.md) -- running security tests in CI pipelines
|