Content Security Policy (CSP) and Page Captures

Content Security Policy is a header that limits which scripts, styles and assets a page may load. How it blocks injected code in captures, and how to bypass it.

By Simon Chiu · Updated

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/>&lt;style&gt; or &lt;script&gt;"] --> 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.

To capture pages with injected CSS and no browser to manage, get a free API key.

Frequently asked questions

Does CSP stop a page from being screenshotted?
No. CSP controls what the page itself may load and run. A headless browser can still load and capture the page. CSP only gets in the way when your automation adds scripts or styles to the page.
Is bypassing CSP in a screenshot tool a security risk?
For the capture itself, the risk is small: the policy is turned off only in that one throwaway browser session, so your own injected code can run. Do not bypass CSP in a session where you are logged in and also load untrusted third-party content.
Why does page.evaluate work on a page with a strict CSP?
Code you run through the DevTools Protocol, such as Puppeteer's page.evaluate, is not treated as page script, so script-src does not block it. Inline script and style tags you add to the DOM are treated as page content and are blocked.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.