@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.
Files changed (89) hide show
  1. package/README.md +293 -0
  2. package/extensions/dino.ts +70 -0
  3. package/extensions/mabl-mcp.ts +306 -0
  4. package/extensions/playwright-mcp.ts +142 -0
  5. package/package.json +47 -0
  6. package/prompts/create-pr.md +110 -0
  7. package/prompts/generate-bdd-from-url.md +65 -0
  8. package/prompts/generate-pom-from-url-cli.md +48 -0
  9. package/prompts/generate-pom-from-url.md +45 -0
  10. package/prompts/generate-spec-from-feature.md +64 -0
  11. package/prompts/heal-playwright-failures.md +106 -0
  12. package/prompts/setup-playwright.md +81 -0
  13. package/scripts/start-dino.cmd +8 -0
  14. package/scripts/start-dino.mjs +226 -0
  15. package/scripts/start-dino.ps1 +9 -0
  16. package/scripts/start-dino.sh +10 -0
  17. package/skills/acli/SKILL.md +292 -0
  18. package/skills/bdd-gherkin/SKILL.md +552 -0
  19. package/skills/build-review-interface/SKILL.md +96 -0
  20. package/skills/error-analysis/SKILL.md +164 -0
  21. package/skills/eval-audit/SKILL.md +183 -0
  22. package/skills/evaluate-rag/SKILL.md +177 -0
  23. package/skills/generate-synthetic-data/SKILL.md +131 -0
  24. package/skills/mabl-mcp/SKILL.md +79 -0
  25. package/skills/playwright-cli/SKILL.md +246 -0
  26. package/skills/playwright-cli/advanced-workflows.md +679 -0
  27. package/skills/playwright-cli/core-commands.md +375 -0
  28. package/skills/playwright-cli/device-emulation.md +475 -0
  29. package/skills/playwright-cli/request-mocking.md +373 -0
  30. package/skills/playwright-cli/running-custom-code.md +553 -0
  31. package/skills/playwright-cli/screenshots-and-media.md +457 -0
  32. package/skills/playwright-cli/session-management.md +425 -0
  33. package/skills/playwright-cli/storage-and-auth.md +487 -0
  34. package/skills/playwright-cli/test-generation.md +336 -0
  35. package/skills/playwright-cli/tracing-and-debugging.md +501 -0
  36. package/skills/playwright-core/SKILL.md +101 -0
  37. package/skills/playwright-core/accessibility.md +1517 -0
  38. package/skills/playwright-core/angular.md +1086 -0
  39. package/skills/playwright-core/api-testing.md +1623 -0
  40. package/skills/playwright-core/assertions-and-waiting.md +703 -0
  41. package/skills/playwright-core/auth-flows.md +1081 -0
  42. package/skills/playwright-core/authentication.md +1409 -0
  43. package/skills/playwright-core/browser-apis.md +673 -0
  44. package/skills/playwright-core/browser-extensions.md +319 -0
  45. package/skills/playwright-core/canvas-and-webgl.md +494 -0
  46. package/skills/playwright-core/clock-and-time-mocking.md +427 -0
  47. package/skills/playwright-core/common-pitfalls.md +1318 -0
  48. package/skills/playwright-core/component-testing.md +1179 -0
  49. package/skills/playwright-core/configuration.md +731 -0
  50. package/skills/playwright-core/crud-testing.md +945 -0
  51. package/skills/playwright-core/debugging.md +827 -0
  52. package/skills/playwright-core/drag-and-drop.md +992 -0
  53. package/skills/playwright-core/electron-testing.md +622 -0
  54. package/skills/playwright-core/error-and-edge-cases.md +1181 -0
  55. package/skills/playwright-core/error-index.md +1903 -0
  56. package/skills/playwright-core/file-operations.md +749 -0
  57. package/skills/playwright-core/file-upload-download.md +1012 -0
  58. package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
  59. package/skills/playwright-core/flaky-tests.md +883 -0
  60. package/skills/playwright-core/forms-and-validation.md +1056 -0
  61. package/skills/playwright-core/i18n-and-localization.md +622 -0
  62. package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
  63. package/skills/playwright-core/locator-strategy.md +588 -0
  64. package/skills/playwright-core/locators.md +743 -0
  65. package/skills/playwright-core/mobile-and-responsive.md +1669 -0
  66. package/skills/playwright-core/multi-context-and-popups.md +581 -0
  67. package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
  68. package/skills/playwright-core/network-mocking.md +1395 -0
  69. package/skills/playwright-core/nextjs.md +1013 -0
  70. package/skills/playwright-core/performance-testing.md +608 -0
  71. package/skills/playwright-core/react.md +1082 -0
  72. package/skills/playwright-core/search-and-filter.md +1366 -0
  73. package/skills/playwright-core/security-testing.md +624 -0
  74. package/skills/playwright-core/service-workers-and-pwa.md +524 -0
  75. package/skills/playwright-core/test-architecture.md +569 -0
  76. package/skills/playwright-core/test-data-management.md +1209 -0
  77. package/skills/playwright-core/test-organization.md +947 -0
  78. package/skills/playwright-core/third-party-integrations.md +754 -0
  79. package/skills/playwright-core/trace-analysis.md +161 -0
  80. package/skills/playwright-core/visual-regression.md +1027 -0
  81. package/skills/playwright-core/vue.md +1162 -0
  82. package/skills/playwright-core/websockets-and-realtime.md +623 -0
  83. package/skills/playwright-core/when-to-mock.md +827 -0
  84. package/skills/playwright-pom/SKILL.md +94 -0
  85. package/skills/playwright-pom/page-object-model.md +921 -0
  86. package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
  87. package/skills/validate-evaluator/SKILL.md +215 -0
  88. package/skills/write-judge-prompt/SKILL.md +144 -0
  89. 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