Content Security Policy (CSP) is a security header, Content-Security-Policy, that tells the browser which sources a page may load scripts, styles, images, fonts and frames from, and whether inline code may run.
Sites use it to limit the damage of cross-site scripting. Screenshot tools meet it from the other side: a strict policy blocks the CSS or JavaScript you inject to hide a banner or prepare the page. This entry is part of the screenshot glossary.
What it means in plain terms
A policy is a list of directives. Each one names a type of resource and the sources allowed for it:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self'; img-src * data:
Read it as: by default, load only from my own origin; scripts may also come from one CDN; styles only from my origin; images from anywhere. Anything not on the list is blocked, and the browser logs a violation. Inline <script> and <style> blocks are blocked unless the policy has 'unsafe-inline', a matching hash, or a nonce.
The policy can arrive as an HTTP header or as a <meta http-equiv="Content-Security-Policy"> tag. A few directives, such as frame-ancestors, work only as a header.
What it changes in a capture
flowchart LR A["Your tool adds<br/><style> or <script>"] --> B{"Page CSP allows<br/>inline code?"} B -->|no| C["Blocked: banner still visible,<br/>style not applied"] B -->|yes| D[Injected code runs] E["Bypass CSP for the session"] --> D F["DevTools evaluate<br/>(page.evaluate)"] -->|not checked by script-src| D
We built a test page with default-src 'self'; script-src 'self'; style-src 'self' and tried to inject a red heading color and a new heading text. Results:
| Method | Without bypass | With bypass |
|---|---|---|
page.addStyleTag({ content }) |
Blocked | Applied |
page.addScriptTag({ content }) |
Blocked | Ran |
page.evaluate(() => { el.style.color = ... }) |
Ran | Ran |
Chromium's error explains the rule: "Applying inline style violates the following Content Security Policy directive 'style-src 'self''. Either the 'unsafe-inline' keyword, a hash (...), or a nonce (...) is required to enable inline execution."
Bypassing CSP in Puppeteer, Playwright and RenderScreenshot
Puppeteer has page.setBypassCSP, which wraps the DevTools command Page.setBypassCSP. Call it before navigation:
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch(); try { for (const bypass of [false, true]) { const page = await browser.newPage(); await page.setBypassCSP(bypass); // must be set before goto await page.goto('http://127.0.0.1:8078/'); try { await page.addStyleTag({ content: 'h1 { color: crimson }' }); await page.addScriptTag({ content: "document.querySelector('#t').textContent = 'Injected'" }); } catch (error) { console.log(`bypass=${bypass} inject failed:`, error.message.split('\n')[0]); } console.log(`bypass=${bypass}`, await page.$eval('#t', (h) => [h.textContent, getComputedStyle(h).color])); await page.close(); } } finally { await browser.close(); }
Output:
bypass=false inject failed: Could not load style bypass=false [ 'Original heading', 'rgb(0, 0, 0)' ] bypass=true [ 'Injected', 'rgb(220, 20, 60)' ]
In Playwright the setting belongs to the browser context:
import { chromium } from 'playwright'; const browser = await chromium.launch(); try { const context = await browser.newContext({ bypassCSP: true }); const page = await context.newPage(); await page.goto('http://127.0.0.1:8078/'); await page.addStyleTag({ content: 'h1 { color: crimson }' }); console.log(await page.locator('#t').evaluate((h) => getComputedStyle(h).color)); // rgb(220, 20, 60) } finally { await browser.close(); }
In RenderScreenshot, set network.bypass_csp. It matters when you use page.styles or page.scripts on a site with a strict policy:
curl -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "network": { "bypass_csp": true }, "page": { "styles": [".promo-bar { display: none !important }"] } }' \ --output clean.png
With the Node SDK it is .bypassCsp() on TakeOptions. If all you need is to hide or remove elements, try page.hide and page.remove first. The options are in network and page modifications, and customize the page before capture shows the full workflow.
Tested on 2026-10-15 on macOS 27 (arm64) with Node 22.18.0, [email protected] (Chrome 148.0.7778.97, headless: 'shell') and [email protected] against a local Node server that sends the policy above. The curl request was not run for this page.
Common mistakes
- Setting the bypass after
goto. The policy applies when the document loads. Turn the bypass on first, or reload. - Thinking CSP blocks the screenshot. It does not. If the capture is blank, look at wait strategies or bot protection, not CSP.
- Confusing CSP with CORS. CSP is the page limiting itself. CORS is another server allowing a page to read its responses. The fixes are different.
- Bypassing CSP in your own app to make captures work. Do not loosen the policy for real visitors. Bypass it only in the capture session, or render your own templates with a policy that already allows what they need.
Related terms
- CORS
- Chrome DevTools Protocol, where
Page.setBypassCSPlives - Cookie consent banner, a common reason to inject CSS
- Shadow DOM
To capture pages with injected CSS and no browser to manage, get a free API key.