← Back to all reports

Unauthenticated Stored XSS via a Cookie-Consent Banner

Reported Jun 5, 2026
Severity High
Platform Web
Vulnerability Class Stored Cross-Site Scripting (CWE-79) - supply chain
Target Type Auto Insurance Provider
Impact Attacker controls every page for every visitor

The Risk

An attacker with no account and no login could rewrite the front page of an auto insurance company's website so that every visitor saw and ran whatever the attacker wanted. The website relied on an outside company to show its cookie notice, and that outside company let anyone on the internet change and publish that notice without proving who they were. With one anonymous request, an attacker could replace the real page with a convincing fake login form on the genuine address, with a valid security padlock, to harvest customer passwords. It needed no action from the victim at all, since the banner runs the moment the page opens.

The Vulnerability

The insurance company's marketing site embedded a third-party cookie-consent banner service. The banner script runs inside the site's own origin and renders banner configuration fields directly into the page DOM via shadowRoot.innerHTML. For tenants using the custom-design option (this target was one), the banner's raw HTML config fields are written straight into the document with no sanitization:

shadowRoot.innerHTML += settingsMarkup;
shadowRoot.innerHTML += bodyMarkup;

That configuration is created and published through the vendor's management API. The critical flaw: those write and publish endpoints enforce no authentication or authorization whatsoever. An anonymous internet host can read, rewrite, and publish the live banner configuration for the target's banner.

The site does ship a Content-Security-Policy via a meta tag, but it does not stop the attack. Its script-src includes 'unsafe-inline' (so an injected inline event handler runs) and allowlists the vendor origins plus wildcard CDN domains (so attacker-hosted external scripts are also loadable). There is no Subresource Integrity on the vendor script.

The Attack

Every request in the chain was unauthenticated - no cookie, token, or header beyond a standard browser User-Agent and a content type. The attack is a four-step chain: write, publish, render, execute.

Confirming the Missing Authorization

The banner configuration for the live banner was readable with no credentials. The write endpoint, when handed a type-invalid body, returned a 400 validation error - never a 401 or 403 - proving the auth check was simply absent:

PUT /api/banner-config/{configId}
{"styles":12345}

-> {"message":["styles must be an object"],"error":"Bad Request","statusCode":400}

Writes genuinely persisted. Against a throwaway test banner on the same platform, a write returned a stored database row with a real id, confirming the data was committed server-side and not merely echoed back.

Draft, Publish, and Cache

The platform uses a draft model: writes land in a draft, and the render feed serves the published snapshot. An unauthenticated publish call promotes the draft to live. The publish endpoint also returned validation errors rather than auth errors, and a valid publish body returned 200 with the configuration's published flag toggling true, all with no authentication. After a short vendor cache delay of one to two minutes, the new config reaches every visitor.

The Payload

To prove code execution safely, a banner registered on the same platform but owned by the tester was used. A single unauthenticated write placed a full-screen takeover into the config html field, where the injected handler sets an element's text to the live document origin so the visual and the proof of execution are one artifact:

PUT /api/banner-config/{ownConfigId}
{"customDesign":true,
 "html":"<div style='position:fixed;inset:0;z-index:2147483647'>
   ...SITE COMPROMISED...
   <img src=x onerror=\"this.parentNode.textContent=document.domain\">
 </div>"}

PUT /api/banner-config/{ownConfigId}/publish
{"name":"poc"}

Both returned 200 with no authentication. After the cache refreshed, a plain page reload replaced the entire page with the attacker-controlled overlay, and the injected script rendered the live origin into the page, proving arbitrary JavaScript executed in the page origin. The only difference between this and the production target was the configuration id in the URL.

The Impact

The injected script executes only in the marketing-site origin. The authenticated application, the login provider, and the payment flow live on separate subdomains that do not embed this banner, so this is not direct session theft or account takeover. The realistic impact is everything arbitrary script in the flagship marketing origin enables:

  • Full page control and defacement: replace what every visitor sees on the primary brand domain. Reputational and brand damage on its own.
  • Phishing from the genuine domain: inject a fake login or "continue your quote" form on the real domain, with valid TLS and no certificate warning. Credentials captured there are valid application credentials. This defeats normal "check the URL" user hygiene.
  • Quote-funnel interception: the insurance quote starts on the marketing site and hands off to the application subdomain. Attacker script can capture data entered before handoff and tamper with the handoff target.
  • Malware and redirect delivery, and reading any non-httpOnly cookies or analytics in the origin.

Two further multipliers raise the severity. It is fully unauthenticated, persistent, and requires no victim interaction, since the consent banner renders on page load. And because one write affects every site that embeds this vendor's banner, the blast radius is a supply-chain one: a single change can hit many tenants at once.

Remediation

The root cause is the vendor: missing authentication and authorization on the banner write and publish endpoints. That must be fixed at source. The mitigations within the site owner's own control are:

  • Tighten the Content-Security-Policy: remove 'unsafe-inline' from script-src and drop the wildcard CDN allowlist, so an injected inline handler or external script cannot run.
  • Pin or self-host the consent layer, or add Subresource Integrity to the vendor script so unexpected content is rejected.
  • Treat the consent vendor as an active supply-chain dependency: evaluate its authorization model before embedding, and have a plan to replace it.
  • Vendor side: enforce authentication and per-tenant authorization on all banner read, write, and publish endpoints, and sanitize or strictly template the config HTML fields rather than writing them straight into the DOM.

All testing was non-destructive. Every probe against the live target's banner was captured and restored byte-for-byte, and the destructive payload was demonstrated only on a banner owned by the tester. No real visitors were affected.