← Blog

You'd never ship code to a server with no controls. So why the browser?

We wrap our servers in layer after layer of controls, then ship the same code to a machine we do not run and add almost nothing. What the browser layer is, and how to start it.

Michael Cameron5 min readUpdated

Picture the controls around one service you run. A firewall in front of it. A WAF reading the requests. Certificates watched, expiry alerted. Dependency scanning on the build, static analysis on the code, secrets in a vault with an audit trail. Logs shipped to a SIEM, and a human review before anything reaches production.

Now picture the controls around the same code once it has been sent to a browser. For most teams the list is a TLS certificate.

It is the same code, handling the same customers’ data. In the browser it runs next to scripts from four vendors, one of which loaded two more after the page rendered, from hosts nobody on the team has heard of. Nobody would deploy to a server on those terms. We do it to the browser every release.

The browser is part of your environment

The usual objection is that the browser is not your infrastructure. That much is true: you do not run it, you cannot patch it, and you cannot put an agent on it that you trust.

But your code runs there, and your customers type their card numbers there. A compromise there is a compromise of your product, whoever hosts the machine. Defence in depth that stops at the machines you own stops one layer short of the person you are protecting.

The controls already exist

Browsers have enforced controls on page content for years, and every one of them is a response header:

  • Content-Security-Policy decides which scripts, styles, frames and network destinations a page may use.
  • Trusted Types, set through CSP, stops a string reaching a DOM sink that would execute it unless it has been through a policy your own code wrote.
  • Integrity-Policy refuses a script that arrives without integrity metadata, which covers a tag that leaves out the integrity attribute.
  • Permissions-Policy switches off the powerful features a page and its frames may reach, such as the camera, microphone, geolocation and payment request.
  • COOP and COEP cut the window references and embedded resources that let another document reach into yours.

Nothing is installed: no agent, no proxy, nothing in your request path. The browser enforces these already. You only have to tell it what to enforce.

And they report

Each of those controls has a report-only twin, Content-Security-Policy-Report-Only, Integrity-Policy-Report-Only and Permissions-Policy-Report-Only, which changes nothing about how the page behaves. The browser applies the policy, notices what it would have refused, and posts a report instead of refusing.

One Reporting-Endpoints header names where those reports go, and every policy points at it. So the second and third control cost no more integration than the first.

This makes the layer safe to add to a site already in production. Ship report-only, read what arrives for a week or two, and fix what it finds. On a first pass that is almost always your own: an image URL still on http://, an inline handler in a template, a vendor host nobody remembered. When the reports go quiet, rename the header to the enforcing one. The policy now blocks, and the reports keep arriving, so you also hear about what it blocks.

What your other layers cannot see

A WAF reads requests arriving at your servers. A skimmer reads the card field in the browser and posts a copy to a host of its own. That request goes from your visitor to the attacker and never touches your edge.

A SIEM reads logs from systems you run. Your servers logged a normal page render. The malicious part ran afterwards, in the browser, and wrote to nothing you collect. The transaction completed, so no business metric moved either, and these breaches run for months.

SAST and SCA read the code you wrote and the packages you chose. A third-party tag is in neither. It is fetched at run time from a host the vendor controls, and it can change between two page loads with no deploy on either side.

A header scan or a DAST run reads one fetch of one page from one address at one moment. It finds a missing header, and misses the tag your tag manager serves to signed-in visitors in another country.

Only the browser saw what ran, and a policy with reporting asks it to tell you.

Where HeaderHawk comes in

We are the endpoint those headers report to, and the screen over what arrives. Point a site’s reporting header at HeaderHawk and it collects CSP violations and every other Reporting API type: integrity, permissions, cross-origin and document policy violations, deprecated APIs, failed requests and crashes. That is on every plan, each type with its own switch and sample rate.

Thousands of reports arrive, and most of the early ones come from somebody’s ad blocker. So violations are grouped by directive, blocked source and page, and a group opens onto the file, line and script sample the browser sent. Reports from extension URLs, from data:, about: and blob: sources and from known bots are classified as noise. They stay visible in the dashboard and never raise an alert.

On every plan, you get an alert when a Content-Security-Policy your site has not been sending turns up in its reports. On the Team plan and above, a fingerprint of each script your visitors’ Chrome and Edge browsers ran. On the Professional and Business plans, you mark your payment pages and authorize or reject each script on them with a written justification, and the inventory exports as CSV. You get an alert when a new, changed or rejected script runs there, once two networks have reported it. A daily scan of those pages alerts when a watched security header changes or the grade drops. It also fetches and hashes every script a payment page’s HTML names, so a script your visitors all have cached still has a fingerprint, and it raises the same script alerts without waiting for a second network.

We do not control anything. Your policy controls, the browser enforces, and we show you whether it held. A rejected script keeps running until your site or your policy stops it.

Where to start

With the headers the setup writes, on one site, in report-only, and a place for the reports to land.

Your least important site is a fine first choice. It will produce a list of things you did not know your pages did. Before this, the browser was the only layer of your stack with no telemetry coming back from it.

What client-side security is covers every control and how each is rolled out. Configs for eight platforms show where the header goes on nginx, Apache, Next.js, Node.js, Docker, Cloudflare, Vercel and Netlify. The Free plan takes the reports with no card, for as long as the rollout takes.

Read your own reports this way

Point your policy's reporting at HeaderHawk and see violations grouped by directive, blocked source and page. The Free plan covers 1 site.