Content Security Policy (CSP) controls where a page may load scripts from. Subresource Integrity (SRI) controls whether a script is the exact file you approved. A tampered file served from an allowed host passes CSP and fails SRI. The Brevo compromise altered three embedded scripts on Brevo's own hosts for more than four hours.
Key takeaways
- CSP answers "where from." SRI answers "which bytes." A compromised vendor CDN defeats the first and not the second.
- A wildcard on a vendor's domain trusts every hostname anyone holding that vendor's DNS keys can create, including hostnames created during an attack.
- With
'strict-dynamic', a trusted script passes its trust to every script it loads, and host allowlists are ignored. - SRI only works on a file that does not change. Most vendor loaders are unversioned, so the usual fix is a versioned URL or a self-hosted copy.
- Blast radius is a control too. A marketing widget on a page where an administrator is signed in runs with that administrator's session.
What happened to the Brevo scripts
On September 14, 2026, an attacker used a Brevo Cloudflare API key to deploy a Cloudflare Worker on Brevo's account. According to Brevo's incident write-up, the key was "long-lived," had "full account permissions," and was "stored in application source code." The Worker rewrote responses at the CDN edge, so Brevo's origin servers and files were never modified.
From 16:07 UTC until the Worker was removed at 20:30 UTC, it appended a loader to "three JavaScript files that customers embed on their own websites." Sansec's analysis names them: cdn.brevo.com/js/sdk-loader.js, cdn.brevo.com/js/brevo-conversations.js, and conversations-widget.brevo.com/brevo-conversations.js. The appended code created a new script element pointing at a second-stage file. Brevo says that payload came from attacker-created hostnames on Brevo-owned domains; Sansec lists several, such as cdn2.sendibt1.com.
The second stage did two things. Visitors saw a fake "verify you are human" page telling them to paste and run a command, the social-engineering pattern known as ClickFix. Logged-in WordPress administrators got something quieter: an attempt to install a malicious plugin using their session. Sansec estimated that more than 100,000 sites embed the affected components.
The customer sites' own code did not change. Their HTML still pointed at the same Brevo URLs it always had. That is the property that makes this class of attack worth understanding, and it is the exact case the two browser controls were designed to split between them.
What CSP decides
A script-src directive tells the browser which sources may supply script. The common form for third-party tools is a host allowlist:
Content-Security-Policy:
script-src 'self'
https://cdn.brevo.com
https://conversations-widget.brevo.com
Under that policy, the tampered sdk-loader.js loads, because it came from an allowed host. CSP checks the origin of a script, not its contents. The appended code then runs with the same authority as the rest of the file.
The second stage is where the policy shape matters:
- Exact hosts. A request to
cdn2.sendibt1.comis not on the list, so the browser blocks it and, if reporting is configured, sends a violation report. In this attack, that alone would have broken the chain on that site. - Wildcards on the vendor's domains. A policy such as
https://*.sendibt1.comallows any hostname under that domain. The attacker held keys that could create DNS records on Brevo's zones, so a wildcard trusted hostnames that did not exist until the attack. - Nonces with
'strict-dynamic'. MDN states that the trust given to a nonced or hashed script "shall be propagated to all the scripts loaded by that root script," and "any allowlist or source expressions such as'self'or'unsafe-inline'will be ignored." That is the right design against cross-site scripting. Against a tampered vendor file, it means the loader's second stage inherits trust from wherever it is fetched.
Even the tightest allowlist has a ceiling here. The attacker chose to fetch a second stage. Nothing stopped them from putting the whole payload inside the allowed file, and no host rule inspects what an allowed file contains.
One more detail from Brevo's write-up is easy to misread. The Worker "removed security headers such as Content-Security-Policy" from the responses it rewrote. Those were Brevo's own pages. A customer's CSP header comes from the customer's server and was untouched, which is why a customer policy still mattered on customer sites.
What SRI decides
Subresource Integrity pins a script tag to a hash of the expected file:
<script src="https://cdn.example.com/widget.v4.2.1.js"
integrity="sha384-..."
crossorigin="anonymous"></script>
Per MDN, if the fetched file does not match, the browser "will refuse to load the resource, and return a network error." Supported algorithms are SHA-256, SHA-384, and SHA-512. Cross-origin SRI requires CORS, so the tag needs a crossorigin attribute and the CDN has to send the matching response header.
Against edge tampering, SRI is decisive. The Worker changed the bytes, the hash would not have matched, and the file would not have run. It does not matter which host served it or who controlled that host's DNS.
The catch is operational. A hash pins one exact file, and files like sdk-loader.js are unversioned on purpose: the vendor updates the file behind a stable URL so customers never touch their embed code. Pin a hash to that URL and the next legitimate update breaks the widget. That is why SRI is common for versioned library builds and rare for vendor loaders. Brevo's own remediation list includes "integrity protection for versioned embedded assets where technically possible," which describes the constraint exactly.
SRI also covers only the tag it sits on. If a pinned loader creates further script elements at runtime, those children are checked only if the loader sets integrity on them.
The two controls side by side
| Control | Question it answers | Tampered file on an allowed host | Second stage from a new hostname | Main cost |
|---|---|---|---|---|
| CSP, exact hosts | Where may script come from? | Runs | Blocked | Maintaining the host list as vendors change infrastructure |
| CSP, wildcard on vendor domain | Same, more loosely | Runs | Runs if the hostname is under the wildcard | Trust extends to hostnames created later |
CSP, nonce with 'strict-dynamic' | Which root scripts are trusted? | Runs (the nonce trusts the tag, not the bytes) | Runs; trust propagates | Nonce plumbing on every response |
| SRI on the tag | Is this the exact approved file? | Blocked | Never loaded, because the loader never ran | Needs a versioned URL and CORS; breaks on vendor updates |
| Self-hosted pinned copy | Who controls the bytes? | Not affected by vendor edge tampering | Depends on your CSP | You own updates and the vendor's release notes |
The table is the whole argument. The controls are not substitutes, and the strongest position uses both: SRI where the file can be pinned, a tight CSP everywhere, and violation reporting turned on so a blocked second stage is noticed rather than silently absorbed.
A practical routine for third-party scripts
- Inventory what loads. List every third-party script on every page template, including tags added through a tag manager. You cannot pin or restrict what you have not listed.
- Pin what can be pinned. Where a vendor publishes versioned builds, reference the versioned URL with
integrityandcrossorigin. Where it does not, consider self-hosting a reviewed copy and updating it on a schedule you control. - Prefer exact hosts to wildcards. Ask each vendor which hostnames its script actually needs. A wildcard is a statement about the vendor's future DNS, not its current behavior.
- Turn on reporting. A
report-toendpoint turns a blocked load into a signal. The newerIntegrity-Policyheader can require integrity metadata on scripts; check current browser support, and run it in report-only mode first to see which tags lack it. - Keep third-party script off privileged pages. The WordPress payload worked because the widget ran where administrators were signed in. Marketing and chat widgets rarely need to load on admin, account, or checkout pages. Where one must, a sandboxed iframe on a separate origin keeps it away from the page's session.
Frequently asked questions
Does Content Security Policy stop a compromised third-party script?
Not if the tampered file is served from a host your policy allows. CSP checks where a script comes from, not what it contains. A strict host allowlist can still block what the tampered file tries to load next from a host that is not on the list.
What is the difference between CSP and Subresource Integrity?
CSP is a response header that limits which sources a page may load resources from. SRI is an attribute on an individual script or stylesheet tag that pins it to a cryptographic hash. CSP restricts origins; SRI verifies exact contents.
Why don't more sites use SRI on vendor scripts?
Because most vendor loaders live at a stable, unversioned URL that the vendor updates in place. A pinned hash breaks on every legitimate update. SRI fits versioned files, or a copy you host yourself.
Is 'strict-dynamic' unsafe?
No. It is a sound defense against injected script, because an attacker cannot forge the nonce. It simply does not address a trusted file whose contents changed, and it lets that file load further scripts from any host.
Would SRI have stopped the Brevo attack?
On a page that referenced the affected files with a matching integrity hash, yes: the altered bytes would not have matched and the browser would have refused to run them. In practice those files were unversioned loaders, so few sites could have pinned them.
What should I check on my own site after a vendor script compromise?
Follow the vendor's advisory first. Brevo told customers to check WordPress sites for plugins installed on September 14 and to treat machines where the command was run as infected. Then review which pages load the vendor's script and whether any of them are privileged.
Where to read next
Browser controls are one layer of a testing program, not the whole of it. For how runtime testing, code analysis, and composition analysis divide the work, read What You Should Know About Application Security Testing.