| title | CSP blocks JavaScript injection in browser automation of authenticated pages | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| domain | mcp | ||||||||
| subdomain | browser-automation | ||||||||
| tags |
|
||||||||
| status | published | ||||||||
| confidence | 0.8 | ||||||||
| created | 2026-07-06 | ||||||||
| updated | 2026-07-06 | ||||||||
| source | zsxh1990 | ||||||||
| verified_date | 2026-07-06 |
Browser automation tools (Puppeteer, Playwright, CDP-based tools) fail silently when injecting JavaScript into pages with strict Content Security Policy headers.
Symptoms:
page.evaluate()returnsnullor throwsEvalError- Injected scripts execute but have no visible effect
Runtime.evaluatevia CDP returns{"result":{"type":"undefined"}}- The page works fine in manual browsing; only automation fails
- Some sites work on first load but fail after login (CSP set per-route)
Common CSP headers that cause this:
Content-Security-Policy: script-src 'self' 'nonce-abc123'
Content-Security-Policy: script-src 'self' 'sha256-...'
Content-Security-Policy: default-src 'self'; script-src 'self'
When script-src does not include 'unsafe-inline' or 'unsafe-eval',
any dynamically injected script without a matching nonce or hash is blocked.
Browser automation tools inject JavaScript in two ways:
eval()/Function(): Creates scripts from strings. Blocked by CSP unless'unsafe-eval'is allowed.- Inline script injection: Creates
<script>elements with text content. Blocked by CSP unless'unsafe-inline'or a matching nonce is present.
Most production sites (banking, enterprise SaaS, government portals) use strict CSP specifically to prevent XSS. This security measure also blocks legitimate automation.
The CSP check happens at the browser level, not the server level.
Even CDP (Chrome DevTools Protocol) respects CSP for Runtime.evaluate.
This method injects a script that runs before any page scripts, bypassing CSP because it executes in the browser's privileged context:
# CDP direct call — works even with strict CSP
await client.send("Page.addScriptToEvaluateOnNewDocument", {
"source": """
// This runs before CSP is applied
window.__automationReady = true;
window.__extractData = () => {
return document.querySelector('.data-table')?.innerText;
};
"""
})
await client.send("Page.navigate", {"url": target_url})
# After navigation, call window.__extractData() via Runtime.evaluateWhy it works: addScriptToEvaluateOnNewDocument injects at the browser level,
before the page's CSP header is parsed. The injected code becomes part of the
page's initial script context.
# Launch Chrome with CSP disabled — NEVER use in production
chrome --disable-web-security --user-data-dir=/tmp/chrome-automationOr via Puppeteer/Playwright:
browser = await puppeteer.launch({
args: ['--disable-web-security'],
headless: true,
})Warning: This disables ALL security protections, not just CSP. Only use for testing on pages you control.
When you only need to read data (not execute logic), use DOM traversal which is not subject to CSP:
# Instead of: page.evaluate("document.querySelector('.price').innerText")
# Use CDP DOM API directly:
result = await client.send("DOM.getDocument", {"depth": 3})
node_id = await client.send("DOM.querySelector", {
"nodeId": result["root"]["nodeId"],
"selector": ".price"
})
text = await client.send("DOM.getOuterHTML", {"nodeId": node_id["nodeId"]})Some pages expose their CSP nonce in a meta tag or global variable. You can reuse it for injected scripts:
nonce = await page.evaluate("""
// Try to find the nonce from existing script tags
document.querySelector('script[nonce]')?.nonce
|| document.querySelector('meta[http-equiv="Content-Security-Policy"]')
?.content?.match(/nonce-([a-zA-Z0-9+/=]+)/)?.[1]
""")
if nonce:
await page.evaluate(f"""
const script = document.createElement('script');
script.nonce = '{nonce}';
script.textContent = 'window.__extractData = () => document.body.innerText';
document.head.appendChild(script);
""")# Check if a page has CSP
curl -sI https://example.com | grep -i content-security-policy
# Or via CDP
python3 -c "
import asyncio, websockets, json
async def check():
async with websockets.connect('ws://localhost:9222/devtools/page/...') as ws:
await ws.send(json.dumps({'id': 1, 'method': 'Network.enable'}))
# Listen for response with CSP header
asyncio.run(check())
"import asyncio
from playwright.async_api import async_playwright
async def test_csp_bypass():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
# Inject before navigation
await page.add_init_script("""
window.__bypass = true;
window.__getData = () => document.title;
""")
await page.goto("https://example.com") # has strict CSP
result = await page.evaluate("window.__bypass")
assert result == True, "CSP bypass failed"
print("CSP bypass works")
await browser.close()
asyncio.run(test_csp_bypass())Expected: CSP bypass works — confirms injection survives CSP.
# This should fail on CSP-protected pages:
try:
result = await page.evaluate("document.title")
print(f"eval works: {result}")
except Exception as e:
print(f"eval blocked by CSP: {e}")Expected on strict-CSP pages: eval blocked by CSP or silent null return.
addScriptToEvaluateOnNewDocumentis the most reliable bypass because it runs before CSP parsing. It works in headless Chrome, Chromium, and all Playwright/Puppeteer versions.- CDP's
Runtime.evaluaterespects CSP.Page.addScriptToEvaluateOnNewDocumentdoes not. This is by design — the latter is a browser-level injection, not a page-level eval. - Some sites use CSP
report-uriorreport-toto log violations. Your automation may trigger violation reports even if the script executes successfully. - For scraping authenticated pages, prefer reading cookies/session from the automation tool's context rather than injecting scripts to extract auth tokens.
- Related:
browser-harness-cdp-browser-automation.md(CDP basics),feishu-block-api-false-success.md(API false success patterns).
- Browsers: Chromium 120+, Chrome 120+, Firefox 120+ (partial CSP bypass support)
- Tools: Puppeteer 21+, Playwright 1.40+, raw CDP via WebSocket
- Platform: Linux, macOS, Windows