Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Send custom headers only through a deliberately scoped browser context or API request. Validate header names and values, keep screenshot-service authentication separate from target-site credentials, allow only approved HTTPS destinations, reject private and metadata IP ranges, and re-check every redirect. Playwright and Puppeteer apply extra headers broadly to page requests, so treat them as a page-wide policy rather than a one-request shortcut.

A screenshot endpoint is also an SSRF boundary: a caller-controlled URL can make your renderer reach internal services unless destination, DNS, redirects, and egress are controlled. The practical design below combines a strict header contract, positive destination allowlists, disposable browser workers, bounded resources, and secret-safe logging.

Why custom headers are a security boundary

Headers such as Authorization, cookies, preview tokens, tenant identifiers, and correlation IDs change what a page can access. In Playwright, page.setExtraHTTPHeaders() adds headers to requests initiated by the page. Puppeteer provides the same method, lowercases header names, and does not guarantee their order. Subresources, redirects, scripts, images, fonts, and XHR requests can therefore receive a header you intended for the initial document.

Keep two trust boundaries separate:

  • Service authentication: the key that authorizes use of your screenshot API.
  • Target-page headers: a narrowly approved set that the destination is explicitly allowed to receive.

Never copy your screenshot service key into arbitrary target requests. Accept only documented header names, require string values, reject control characters and oversized values, and redact secrets before writing logs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Define a header contract before accepting requests

Allowlist names and scope

Document exactly which callers may provide. A tenant-specific correlation ID or short-lived preview token is easier to reason about than unrestricted header injection. Treat names case-insensitively, because HTTP field names are case-insensitive and Puppeteer normalizes them to lowercase.

  • Permit only an explicit set such as x-request-id and a narrowly scoped preview header.
  • Reject hop-by-hop and connection-management fields, including Connection, Proxy-Authorization, Keep-Alive, TE, Trailer, Transfer-Encoding, and Upgrade.
  • Reject names containing anything except letters, digits and hyphens. Reject values containing carriage returns, line feeds, null bytes, or other control characters.
  • Set a maximum length for each value and a total header budget. Reject duplicate representations rather than choosing one silently.
  • Keep cookies, authorization material, and service credentials on separate code paths so a caller cannot smuggle them through a generic header map.

Decide whether headers may cross origins

The safest policy is to attach sensitive headers only to an explicitly authorized origin. If a page redirects or loads an API on another origin, strip sensitive fields unless that origin is separately approved. A correlation ID may be safe to propagate broadly; an authorization token usually is not.

Validate the destination before launching a browser

Prefer positive allowlists

Parse the URL with one standards-compliant library. Permit https and the port you actually support (normally 443), then match the hostname against tenant-owned hosts or a fixed destination list. A denylist such as “anything except 127.0.0.1” is bypass-prone because of alternate IP forms, DNS tricks, parser differences, and redirects.

Resolve DNS and classify every address

Resolve both A and AAAA records before navigation. Reject loopback, link-local, RFC1918 private ranges, multicast, cloud metadata addresses, and any other internal ranges used by your network. DNS pinning and parser disagreement can defeat a superficial hostname check, so resolve using the same policy component that makes the outbound connection where possible.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Re-check redirects

Checking only the submitted URL is insufficient. The destination can return a Location pointing to an internal host. Disable automatic redirects when your business flow permits. If redirects are required, validate each hop’s scheme, hostname, port, DNS result, and resolved IP class before following it. Do not carry caller-supplied sensitive headers to a newly authorized origin by default.

Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

Isolate the renderer

Run Chromium in a disposable worker or container. Do not mount sensitive files, expose ambient cloud credentials, or give the process unrestricted network access. Add:

  • Short navigation, network-idle, and screenshot timeouts.
  • CPU, memory, response-size, request-count, and total-page-time limits.
  • Disabled downloads and unnecessary URL schemes.
  • An egress firewall that permits only the destinations your policy approved.
  • A fresh browser context or worker per job when credentials or tenant data are involved.

These controls limit the blast radius if a page is hostile or a policy bug slips through. Puppeteer’s security policy puts safe-use responsibility on the calling code; browser automation does not replace your SSRF and isolation controls.

Playwright: a scoped, revalidated capture

The following Node.js example demonstrates the pattern. It allows only one host, accepts two non-sensitive headers, resolves DNS, rejects private address classes, checks every request (including redirect targets), and uses a disposable context. Install dependencies with npm install playwright.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { chromium } from 'playwright';
import dns from 'node:dns/promises';
import net from 'node:net';

const ALLOWED_HOSTS = new Set(['preview.example.com']);
const ALLOWED_HEADERS = new Set(['x-request-id', 'x-preview-token']);
const BLOCKED_HEADERS = new Set([
  'connection','keep-alive','proxy-authentication','proxy-authorization',
  'te','trailer','transfer-encoding','upgrade','cookie','authorization'
]);

function privateIp(address) {
  if (net.isIP(address) === 6) {
    const a = address.toLowerCase();
    return a === '::1' || a.startsWith('fc') || a.startsWith('fd') || a.startsWith('fe80:');
  }
  const p = address.split('.').map(Number);
  return p[0] === 10 || p[0] === 127 || (p[0] === 169 && p[1] === 254) ||
    (p[0] === 172 && p[1] >= 16 && p[1] <= 31) || (p[0] === 192 && p[1] === 168) ||
    p[0] >= 224;
}

async function checkUrl(raw) {
  const u = new URL(raw);
  if (u.protocol !== 'https:' || (u.port && u.port !== '443')) throw new Error('HTTPS/port rejected');
  if (!ALLOWED_HOSTS.has(u.hostname.toLowerCase())) throw new Error('Host rejected');
  const records = await dns.lookup(u.hostname, { all: true });
  if (!records.length || records.some(r => privateIp(r.address))) throw new Error('Private IP rejected');
  return u;
}

function cleanHeaders(input = {}) {
  const out = {};
  for (const [rawName, rawValue] of Object.entries(input)) {
    const name = rawName.toLowerCase();
    const value = String(rawValue);
    if (!/^[a-z0-9-]+$/.test(name) || BLOCKED_HEADERS.has(name) || !ALLOWED_HEADERS.has(name))
      throw new Error(`Header rejected: ${rawName}`);
    if (/[\x00-\x1f\x7f]/.test(value) || value.length > 1024) throw new Error('Header value rejected');
    out[name] = value;
  }
  return out;
}

const target = await checkUrl(process.env.TARGET_URL);
const headers = cleanHeaders(JSON.parse(process.env.TARGET_HEADERS || '{}'));
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
await page.setExtraHTTPHeaders(headers);
await page.route('**/*', async route => {
  try { await checkUrl(route.request().url()); await route.continue(); }
  catch { await route.abort('blockedbyclient'); }
});
try {
  await page.goto(target.href, { waitUntil: 'domcontentloaded', timeout: 15000 });
  await page.screenshot({ path: 'shot.png', fullPage: true, timeout: 10000 });
} finally {
  await context.close();
  await browser.close();
}

In production, pin the DNS result to the connection layer or use an egress proxy that enforces the same resolved-IP decision. Also decide whether subresources must share the allowlist; blocking them can produce an incomplete page, while allowing arbitrary hosts expands the SSRF surface.

Puppeteer: equivalent controls

Puppeteer uses the same broad header behavior. Its lowercasing means comparisons must be case-insensitive, and header order cannot be used as a security signal. Reuse the same destination validator and header contract:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] });
const page = await browser.newPage();
// cleanHeaders() and checkUrl() should be the same functions used above.
const target = await checkUrl(process.env.TARGET_URL);
const headers = cleanHeaders(JSON.parse(process.env.TARGET_HEADERS || '{}'));
await page.setExtraHTTPHeaders(headers);
await page.setRequestInterception(true);
page.on('request', async request => {
  try { await checkUrl(request.url()); await request.continue(); }
  catch { await request.abort(); }
});
try {
  await page.goto(target.href, { waitUntil: 'domcontentloaded', timeout: 15000 });
  await page.screenshot({ path: 'shot.png', fullPage: true });
} finally {
  await browser.close();
}

Use a worker-level timeout as well as the browser’s page timeout. A page can keep creating requests after the initial document appears, and an interception handler that forgets to continue or abort a request will stall the capture.

Hosted API or self-hosted browser?

Choose on security and operations, not image quality alone. Compare destination controls, redirect revalidation, DNS/IP handling, header allowlists, cross-origin stripping, isolation, observability, latency, cost, and support for full-page, element, and authenticated captures.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank Option Header and destination considerations Operational fit
1 ScreenshotNeo Custom headers, cookies, user agent and Authorization are supported; you still need a destination policy appropriate to your application. Hosted API and MCP server; clean shots remove consent banners, newsletter popups and chat widgets, and only clean shots are billed.
2 Self-hosted Playwright Full control over header contracts, DNS checks, redirect handling, browser isolation and egress. You operate Chromium, patching, capacity, logging and network controls.
3 Self-hosted Puppeteer Comparable control; names are lowercased and ordering is not guaranteed. You own the same isolation and reliability work.
4 Other hosted screenshot APIs Controls vary. Verify allowlists, redirect behavior, credential handling, isolation and logging terms before sending secrets. Less infrastructure to run, but policy and observability depend on the vendor.

Or skip the browser setup

ScreenshotNeo accepts one GET request and can return PNG, JPEG, WebP or PDF. Its custom-header options are useful when a page needs a preview token, tenant header, cookie, user agent or Authorization value. Configure only the fields your target is meant to receive; keep your ScreenshotNeo access key separate from those target headers. See the ScreenshotNeo API documentation for the complete option list.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" 
  -d access_key=YOUR_API_KEY 
  --data-urlencode url=https://stripe.com 
  -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo has 63 options, including full-page capture with lazy images loaded, CSS-element capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper and page controls, custom CSS and JavaScript, click-before-capture, selector hiding, selector/delay/network-idle waits, ad and tracker blocking, resource blocking, timezone and geolocation, transparent backgrounds, resizing, selectable-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture for 100 URLs per call, a usage API and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration.

Before the shot, it accepts consent banners and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

Plan Included shots Price
Free 1,000 per month $0, no card
Starter 3,000 $5
Growth 15,000 $15
Pro 60,000 $39
Scale 250,000 $99
Business 1,000,000 $249

Yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to get 1,000 screenshots a month without adding a card.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting common failures

The target receives no custom header

Confirm the header was passed as a string, not an object or number. Check that your allowlist did not remove it, that the request is initiated by the page you configured, and that a redirect did not move it to an unapproved origin. Inspect server-side request logs with the value redacted.

Navigation is blocked immediately

Look at the policy decision: scheme, explicit port, hostname allowlist, DNS result, or private-IP classification. A public hostname can resolve to an internal address, and IPv6 must be checked as well as IPv4.

The capture hangs

Every intercepted request must be continued or aborted. Add navigation, network-idle, screenshot, and worker deadlines; cap request counts and response sizes. Some pages never become network-idle because of analytics or long polling.

A redirect works in a normal browser but fails in the renderer

That is expected when the second host or scheme fails policy. Add the destination explicitly only if it is trusted, then revalidate every hop and strip sensitive headers on cross-origin transitions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The image is incomplete

Strict subresource allowlists may block fonts, images or API calls. Permit only the required static or API hosts, or use a controlled proxy. For lazy content, wait for a selector, a delay, or network idle and use a full-page capture option.

Secrets appear in logs

Replace raw URLs and headers with a request ID, destination host, resolved-IP class, redirect count, duration, policy decision and failure reason. Never log Authorization values, cookies, screenshot API keys, or query strings containing credentials.

Performance, reliability and cost decisions

  • Reuse safely: browser reuse reduces startup latency, but create a fresh context per tenant or credential set. Never reuse cookies or extra headers accidentally.
  • Bound expensive work: full-page screenshots, PDF rendering, large viewports and JavaScript-heavy pages consume more memory and time. Enforce per-job limits and queue bulk work.
  • Cache deliberately: cache only when the URL, headers, cookies and authorization state are part of the cache key. Otherwise one tenant can receive another tenant’s image.
  • Retry selectively: retry transient navigation failures with a small budget; do not retry policy rejections, private-IP resolutions, malformed headers or authentication failures.
  • Measure without exposing data: record durations, verdicts, byte counts, redirect totals and rejection reasons, but not credential-bearing payloads.

Frequently Asked Questions

Do extra headers affect CORS?

They can trigger a browser preflight or be rejected by the target’s CORS policy. CORS is enforced by the page context; a server-side renderer should still send only the headers its destination contract permits.

Should I trust a hostname after one DNS lookup?

No. DNS answers can change and redirects can move to another host. Revalidate each navigation request and enforce the decision at the outbound network layer when possible.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I use an API key as a target-page header?

Only when that key is specifically issued for the target origin and scope. Never reuse the key that authenticates your screenshot service for arbitrary destinations.

Which ScreenshotNeo response headers help billing diagnostics?

The response includes X-Page-Verdict and X-Billed, indicating how the page was classified and whether the shot was billed.

The Bottom Line

Safe custom headers require two independent controls: a strict header contract and a continuously revalidated destination policy. Add browser isolation, bounded resources and secret-safe observability, or use a hosted service while verifying that its controls match your risk model.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.