| name | browser-automation |
| version | 2.0.0 |
| description | Browser automation with Puppeteer, Playwright, CDP, and WebDriver for testing, scraping, and web interaction. Use when automating browsers, web scraping, or end-to-end testing. Do NOT use for native app automation. |
| risk_level | HIGH |
| token_budget | 3500 |
Browser Automation Expert - Code Generation Rules
0. Anti-Hallucination Protocol
0.2 Security Patterns (security rules)
CWE-79: XSS via evaluate
- Do not:
page.evaluate(userString) - execute user code
- Instead: Parameterize, use
evaluateHandle with sanitized data
CWE-200: Credential Exposure
- Do not: Screenshot/video with credentials visible
- Instead: Mask sensitive data, use environment variables for creds
CWE-611: SSRF via Navigation
- Do not:
page.goto(userUrl) without validation
- Instead: Allowlist domains, validate URL format
1. Security Principles
1.1 No Arbitrary Code Execution (CWE-94)
Principle: Never execute untrusted code via page.evaluate(). Validate all data.
async function scrape(page: Page, userSelector: string) {
return await page.evaluate(`document.querySelector('${userSelector}')`);
}
async function scrape(page: Page, selector: string) {
if (!/^[a-zA-Z0-9\s\[\]\.#\-_="':>+~*]+$/.test(selector)) {
throw new Error('Invalid selector');
}
return await page.evaluate((sel) => {
return document.querySelector(sel)?.textContent;
}, selector);
}
1.2 Credential Protection (CWE-798)
Principle: Never screenshot pages with credentials. Mask sensitive data.
await page.type('#password', process.env.PASSWORD!);
await page.screenshot({ path: 'debug.png' });
await page.type('#password', process.env.PASSWORD!);
await page.evaluate(() => {
document.querySelectorAll('input[type="password"]').forEach(el => {
(el as HTMLInputElement).value = '********';
});
});
await page.screenshot({ path: 'debug.png' });
1.3 Input Validation (CWE-20)
Principle: Validate all URLs and selectors. Whitelist allowed domains.
1.4 Secrets ≠ Code (CWE-798)
Principle: Credentials from environment/vault only. Never log credentials.
1.5 Fail Secure (CWE-636)
Principle: On error, close browser. Don't leave sessions open.
1.6 Defense in Depth
Principle: Network isolation, proxy support, user-agent rotation.
2. Version Requirements
Use these minimum versions:
{
"dependencies": {
"playwright": "^1.41.0",
"puppeteer": "^22.0.0",
"zod": "^3.22.0"
}
}
3. Code Patterns
3.1 WHEN setting up Playwright with security
import { chromium, Browser, BrowserContext, Page } from 'playwright';
interface BrowserConfig {
proxy?: { server: string; username?: string; password?: string };
allowedDomains: string[];
timeout: number;
}
export class SecureBrowser {
private browser: Browser | null = null;
private context: BrowserContext | null = null;
private config: BrowserConfig;
constructor(config: BrowserConfig) {
this.config = config;
}
async launch(): Promise<void> {
this.browser = await chromium.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-extensions',
'--disable-background-networking',
'--disable-default-apps',
'--disable-sync',
],
});
this.context = await this.browser.newContext({
permissions: [],
geolocation: undefined,
userAgent: 'Mozilla/5.0 (compatible; Bot/1.0)',
proxy: this.config.proxy,
navigationTimeout: this.config.timeout,
});
await this.context.route('**/*', async (route) => {
const url = new URL(route.request().url());
if (!this.config.allowedDomains.some(d => url.hostname.endsWith(d))) {
await route.abort('blockedbyclient');
return;
}
await route.continue();
});
}
async newPage(): Promise<Page> {
if (!this.context) throw new Error('Browser not launched');
return await this.context.newPage();
}
async close(): Promise<void> {
await this.context?.close();
await this.browser?.close();
this.context = null;
this.browser = null;
}
}
async function withBrowser<T>(
config: BrowserConfig,
fn: (browser: SecureBrowser) => Promise<T>
): Promise<T> {
const browser = new SecureBrowser(config);
try {
await browser.launch();
return await fn(browser);
} finally {
await browser.close();
}
}
3.2 WHEN implementing safe page navigation
import { Page } from 'playwright';
import { z } from 'zod';
const UrlSchema = z.string().url().refine(
(url) => {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
},
{ message: 'Only HTTP(S) URLs allowed' }
);
const ALLOWED_DOMAINS = ['example.com', 'api.example.com'];
async function navigateSafely(page: Page, url: string): Promise<void> {
const validatedUrl = UrlSchema.parse(url);
const parsed = new URL(validatedUrl);
if (!ALLOWED_DOMAINS.some(d => parsed.hostname === d || parsed.hostname.endsWith(`.${d}`))) {
throw new Error(`Domain not allowed: ${parsed.hostname}`);
}
await page.goto(validatedUrl, {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
const currentUrl = new URL(page.url());
if (!ALLOWED_DOMAINS.some(d => currentUrl.hostname === d || currentUrl.hostname.endsWith(`.${d}`))) {
throw new Error(`Unexpected redirect to: ${currentUrl.hostname}`);
}
}
3.3 WHEN extracting data safely
import { Page } from 'playwright';
import { z } from 'zod';
const ProductSchema = z.object({
title: z.string().min(1).max(500),
price: z.number().positive(),
description: z.string().max(5000).optional(),
url: z.string().url(),
});
type Product = z.infer<typeof ProductSchema>;
async function extractProduct(page: Page): Promise<Product | null> {
const rawData = await page.evaluate(() => {
const title = document.querySelector('h1')?.textContent?.trim();
const priceText = document.querySelector('.price')?.textContent;
const price = priceText ? parseFloat(priceText.replace(/[^0-9.]/g, '')) : null;
const description = document.querySelector('.description')?.textContent?.trim();
return {
title,
price,
description,
url: window.location.href,
};
});
const result = ProductSchema.safeParse(rawData);
if (!result.success) {
console.error('Invalid data extracted:', result.error);
return null;
}
return result.data;
}
3.4 WHEN handling authentication securely
import { Page, BrowserContext } from 'playwright';
interface Credentials {
username: string;
password: string;
}
async function authenticateSecurely(
context: BrowserContext,
loginUrl: string,
credentials: Credentials
): Promise<void> {
const page = await context.newPage();
try {
await page.goto(loginUrl);
await page.fill('#username', credentials.username);
await page.fill('#password', credentials.password);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard**', { timeout: 10000 });
const isLoggedIn = await page.evaluate(() => {
return !!document.querySelector('.user-menu');
});
if (!isLoggedIn) {
throw new Error('Authentication failed');
}
await context.storageState({ path: 'auth-state.json' });
} finally {
await page.evaluate(() => {
document.querySelectorAll('input[type="password"]').forEach(el => {
(el as HTMLInputElement).value = '';
});
});
await page.close();
}
}
async function withAuthenticatedContext(
browser: Browser,
fn: (context: BrowserContext) => Promise<void>
): Promise<void> {
const context = await browser.newContext({
storageState: 'auth-state.json',
});
try {
await fn(context);
} finally {
await context.close();
}
}
3.5 WHEN implementing rate limiting
import { Page } from 'playwright';
class RateLimitedScraper {
private lastRequest = 0;
private minDelay: number;
private requestCount = 0;
private maxRequests: number;
constructor(minDelayMs: number = 1000, maxRequestsPerSession: number = 100) {
this.minDelay = minDelayMs;
this.maxRequests = maxRequestsPerSession;
}
async scrape<T>(page: Page, fn: () => Promise<T>): Promise<T> {
if (this.requestCount >= this.maxRequests) {
throw new Error('Request limit exceeded');
}
const now = Date.now();
const elapsed = now - this.lastRequest;
if (elapsed < this.minDelay) {
await new Promise(r => setTimeout(r, this.minDelay - elapsed));
}
this.lastRequest = Date.now();
this.requestCount++;
const jitter = Math.random() * 500;
await new Promise(r => setTimeout(r, jitter));
return await fn();
}
}
const scraper = new RateLimitedScraper(2000, 50);
const data = await scraper.scrape(page, async () => {
await page.goto('https://example.com/data');
return await page.evaluate(() => document.body.textContent);
});
3.6 WHEN handling errors and cleanup
import { Browser, BrowserContext, Page } from 'playwright';
class BrowserSession {
private browser: Browser;
private context: BrowserContext;
private pages: Set<Page> = new Set();
constructor(browser: Browser, context: BrowserContext) {
this.browser = browser;
this.context = context;
}
async newPage(): Promise<Page> {
const page = await this.context.newPage();
this.pages.add(page);
page.on('close', () => this.pages.delete(page));
return page;
}
async cleanup(): Promise<void> {
for (const page of this.pages) {
try {
await page.close();
} catch {
}
}
this.pages.clear();
try {
await this.context.close();
} catch {
}
try {
await this.browser.close();
} catch {
}
}
}
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
delayMs: number = 1000
): Promise<T> {
let lastError: Error | null = null;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (lastError.message.includes('net::ERR_NAME_NOT_RESOLVED')) {
throw lastError;
}
if (i < maxRetries - 1) {
await new Promise(r => setTimeout(r, delayMs * Math.pow(2, i)));
}
}
}
throw lastError;
}
4. Anti-Patterns
Do not:
- Execute user-provided code via page.evaluate()
- Screenshot pages with visible credentials
- Navigate to non-whitelisted domains
- Leave browser sessions open on error
- Log credentials or sensitive data
- Scrape without rate limiting
- Trust data from page without validation
5. Testing
ALWAYS write automation tests:
import { test, expect } from '@playwright/test';
test.describe('Scraper Security', () => {
test('rejects invalid selectors', async ({ page }) => {
const maliciousSelector = "'); alert(1); //";
await expect(
# ... (additional test cases follow same pattern)
6. Pre-Generation Checklist
Before generating any browser automation code: