SecurityGENERALBROWSER-SPECIFICSPEC-EVOLVING

Third-Party Scripts and the Supply Chain

A script tag grants full authority over your page. There is no partial trust, and the dependency you never chose is running in the same context as your login form.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

What am I actually granting when I add someone else's code to my page?

The user intent

Marketing wants analytics, support wants a chat widget, and finance wants a payment script. All three are reasonable asks that arrive as a one-line snippet.

The obvious build

Paste the snippet in the head. It is one line and the vendor is a well-known company, so the risk is theirs.

Why it breaks

A script tag runs with your page's full authority. It can read every cookie your JavaScript can read, every field in every form, and everything in browser storage. There is no scope on that grant (The Browser Security Model).

How it breaks in a real browser
  • A script tag runs with your page's full authority. It can read every cookie your JavaScript can read, every field in every form, and everything in browser storage. There is no scope on that grant (The Browser Security Model).
  • You did not audit the code, and it is fetched fresh on every load — so what you reviewed and what runs are different things, indefinitely.
  • Most third-party scripts load further scripts from origins you were never told about, so the trust boundary you extended is not the one you thought.
  • A compromise anywhere in that chain is a compromise of your login form, and it is invisible in your own repository and your own review process (Cross-Site Scripting).
  • It executes on your main thread, on a schedule you do not control and cannot profile from outside (Long Tasks).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • The same-origin policy protects you from *other pages*. It does nothing about code you deliberately execute inside your own origin — a third-party script is same-origin the moment you include it (The Same-Origin Policy).
  • The browser offers exactly two coarse grants: inline in your page (full authority) or inside a cross-origin iframe (its own origin, its own storage, no access to your DOM). There is no middle setting on a plain script tag.
  • Subresource Integrity lets you pin a hash so the browser refuses a file whose bytes changed. It protects against a modified file at a known URL; it does not protect against a vendor deliberately shipping a new, malicious version at a new URL, and it cannot be used for a script whose content changes by design.
  • CSP constrains which origins may execute at all, which turns "anyone who compromises any of our vendors" into "anyone who compromises the vendors we listed" (Content Security Policy).
  • The supply chain extends to build time as well: a dependency pulled in by your bundler runs with the same authority, and arrives without even a snippet to review (The Module Graph).

What this makes the browser do

And which of it is avoidable.

  • An extra origin means a DNS lookup, a connection and a TLS handshake before a single byte of the script arrives — often on the critical path (Reading a Network Waterfall).
  • Parsing, compiling and executing code you did not write, on the thread that owes the user a frame (The Real Cost of JavaScript).
  • Whatever the script then does: DOM mutation, layout reads, its own network requests, its own timers, and often its own further script loads.
  • Memory retained by a widget for the life of the page, outside anything your profiling attributes to your own components (Memory Leaks).

There is no partial trust

The platform gives you two settings, and the gap between them is enormous. A script in your page is you: same origin, same cookies, same storage, same DOM, same API access. A cross-origin iframe is someone else: separate origin, separate storage, no reach into your document at all.

Almost every third-party integration decision is really a decision about which of those two you are choosing, and it is usually made by pasting whichever snippet the vendor supplied. Making it deliberately is most of the work.

  • The middle column is the whole lesson: async and defer change *when* code runs, never *what it may do* (`defer`, `async` and `type="module"`).
  • A vendor that "cannot work in an iframe" is telling you it needs access to your page. That may be legitimate; it is always worth hearing out loud.
  • A build-time dependency is a third-party script with worse ergonomics for review — it never even appears as a tag someone could notice.
GrantCan read your DOM and formsCan read cookies and storageCan call your API as the userContained if compromised
Inline script tagYes, entirelyYes, everything script-readableYes — credentials attach automaticallyNo
Async / deferred scriptYes, entirelyYesYesNo — timing changes, authority does not
Cross-origin iframeNoNo — separate originNoYes
iframe with sandboxNoNoNoYes, plus restricted capabilities
Build-time dependencyYes, entirelyYesYesNo — and no snippet to review

Reducing the grant

Because the platform's grants are coarse, the practical work is choosing the smallest one that still delivers the feature, and then constraining what is left. These three controls compose, and each covers something the others do not.

A team wants to add a third-party script. What should you do with it?

What does this vendor genuinely need access to?

Nothing on the page — it renders its own UI

when Chat widgets, embedded media, ads, payment fields.

cost Cross-origin iframe. Real containment, and integration becomes explicit message passing with no shared styling (Clickjacking and Framing).

Card or credential input

when Any field whose value you would rather never possess.

cost A provider-hosted iframe means the data never enters your DOM, which changes your risk and compliance position more than any policy can.

Observation only — events and page metadata

when Analytics, error tracking, performance monitoring.

cost It runs in your origin, so constrain it: CSP allowlist, SRI where the file is stable, loaded off the critical path, and an owner who can answer for it (Content Security Policy).

Deep DOM integration it cannot do otherwise

when Session replay, personalisation, A/B tools that rewrite content.

cost Full authority over the page, including every form. Treat it as a code dependency with a named owner, an inventory entry and a review — and consider excluding it from pages handling credentials or payment (Session Replay and the Privacy It Costs).

It is a build-time package

when Anything your bundler pulls in.

cost Same authority, less visibility. Lockfiles, provenance and a real update policy are the equivalent controls (The Module Graph).

The same widget, two grants
1<!-- full authority over the page: DOM, forms, cookies, your API -->
2<script src="https://widget.example.com/chat.js"></script>
3
4<!-- contained: its own origin, its own storage, no reach into your DOM -->
5<iframe
6 src="https://widget.example.com/chat-frame"
7 sandbox="allow-scripts allow-forms allow-same-origin"
8 title="Customer support chat"
9 loading="lazy"
10></iframe>

The title is not decoration — an untitled iframe is an unlabelled region for a screen reader, and third-party embeds are where that omission is most common.

What actually goes wrong

These are the recurring incidents, and what is striking about them is how few involve anyone attacking you directly. Most are a vendor's change, a vendor's outage, or a vendor's dependency — arriving in your page through a door you left open years earlier and stopped thinking about.

Third-party incidents and their real cause
TriggerSymptomCauseResponse
Vendor ships a new versionPage breaks, or starts doing something new, with no deploy on your sideA live CDN reference means their release schedule is your release schedulePin a version, add SRI where the file is stable, or vendor the dependency into your own build.
Vendor origin is slow or downYour page stalls before renderingA synchronous script in the head blocks parsing on a host you do not controlNever load third-party code synchronously on the critical path (Why a Script Tag Stops the Parser).
Tag manager used by a non-engineerUnknown scripts in production; CSP violations from origins nobody recognisesThe tag manager is a mechanism for adding arbitrary code without reviewTreat tag-manager changes as deploys: review, allowlist, and inventory them (Deploying a Frontend).
Upstream package compromisedMalicious code shipped to every user, signed by your buildA transitive dependency updated inside a permitted rangeLockfiles, deliberate updates, and a CSP that limits where exfiltration could send anything.
Chat widget injected at end of bodyKeyboard users tab through the whole page to reach a floating button; focus is trapped once they doDOM order, not visual order, determines tab order — and the vendor tested with a mouseConstrain it in an iframe, give it a title, and test the flow by keyboard (Keyboard Operability).
Session replay on a checkout pageCard and personal data captured into a vendor's storeReplay captures what is on screen; masking is opt-out-shaped and fails openExclude sensitive routes entirely; never rely on field-level masking as the only control (Session Replay and the Privacy It Costs).

How to build it

Most important first.

  • Ask what authority the thing actually needs. Analytics needs to observe and report; it does not need to read your form fields. If a vendor cannot work inside an iframe, that is information about what it is doing.
  • Sandbox what can be sandboxed. A cross-origin iframe with an explicit sandbox attribute is the only real containment the platform offers, and a chat widget or an ad is usually a candidate (Clickjacking and Framing).
  • Constrain the rest with CSP. An allowlist converts an unbounded risk into an enumerated one, and the violation reports tell you when something unexpected tried to run (Content Security Policy).
  • Use Subresource Integrity for any third-party file whose contents are supposed to be stable, and understand precisely what it does and does not cover.
  • Load it off the critical path unless it genuinely must run first. defer or dynamic injection after interactivity removes the failure mode where a slow vendor delays your own page (`defer`, `async` and `type="module"`).
  • Keep an inventory. Teams routinely cannot answer "what third-party code runs on our checkout page", and that question is the whole of this lesson.
  • Prefer a vendored, versioned dependency over a live CDN reference where you can: it moves the change into your review process instead of leaving it in someone else's deploy pipeline.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Third-party widgets are a common source of accessibility regressions you did not write and cannot easily fix: chat bubbles that trap focus, cookie banners that are not dismissible by keyboard, and overlays that cover content without being announced (Focus Management).
  • A widget that injects itself at the end of the body lands last in the tab order regardless of where it appears visually, so keyboard users reach a floating button after the entire page.
  • Ironically, "accessibility overlay" scripts are themselves a well-documented cause of harm — they override real semantics with guesses, and assistive-technology users frequently report them as making sites less usable, not more. Fix the markup instead (Semantics Before ARIA).
  • A vendor script that blocks the main thread stops accessibility-tree updates for as long as it runs, so a screen reader reads stale content (The Multi-Process Browser).
  • You own the accessibility of your page including the parts you did not write. "It is the vendor's widget" is an explanation, not a defence.

What can go wrong

Failure modes
  • A tag manager that lets non-engineers add arbitrary scripts to production, bypassing review entirely — the most common way a page acquires code nobody knows about.
  • A vendor outage taking your page down because their script was synchronous and render-blocking (Render-Blocking Resources).
  • SRI applied to a URL whose content is intended to change, so the page breaks the next time the vendor ships.
  • A CSP written permissively enough to satisfy every vendor, which is a CSP that permits everything.
  • A widget that adds unremovable listeners and grows memory for the life of the session, attributed in profiles to "the page" rather than to the vendor.
  • A build-time dependency compromised upstream, which ships to every user with your own signature on it.
What can arrive out of order
  • A third-party script that mutates the DOM can race your own rendering, producing hydration mismatches and elements that appear and vanish (Hydration Mismatch).
  • Asynchronously loaded vendor scripts execute in arrival order, so behaviour differs between fast and slow networks in ways that are hard to reproduce (`defer`, `async` and `type="module"`).
Security
  • A third-party script can read anything your own script can read: document.cookie for non-HttpOnly cookies, every value in Web Storage, and the contents of every input on the page (Storage Security and Durability).
  • It can add its own listeners to your forms, so a keylogger is a few lines and is indistinguishable from analytics in a network trace.
  • It can perform authenticated requests to your own API, because it runs in your origin and the browser attaches credentials automatically (Cross-Site Request Forgery).
  • HttpOnly cookies are meaningfully better here: they are unreadable by any script, including one you did not intend to run (Cookies vs Script-Readable Tokens).
  • Payment and credential fields deserve a stronger boundary than a policy. If a form takes card details, an iframe from the payment provider means the data never enters your DOM at all, and that is a materially different risk position.
Misreads
  • "It is from a reputable company, so it is safe." The question is not whether they are trustworthy but whether their entire delivery chain is, forever, and whether you would notice if it stopped being.
  • "CSP means we are covered." CSP limits which origins may execute. An allowed origin that is compromised is still fully allowed (Content Security Policy).
  • "SRI makes third-party scripts safe." It makes a specific file immutable. It says nothing about what that file does.
  • "It is only analytics." Analytics scripts have full DOM access by construction, which is why they are a recurring vector.
  • "The vendor's widget is the vendor's accessibility problem." It is on your page, so it is your users' problem, which makes it yours.

Measuring it, and what changes in the field

How you would see this
  • A CSP in report-only mode is the cheapest inventory tool available: it tells you every origin your pages actually load, including the ones nobody documented (Content Security Policy).
  • The Network panel grouped by domain answers "who is on this page" in about ten seconds (Debugging the Network).
  • Third-party main-thread time is broken out in performance tooling; it is usually a larger share than teams expect (Measure Before Optimising).
  • Field data segmented by whether a vendor script loaded tells you what it actually costs real users (Real User Monitoring).
Slow device, slow network, large data, old tab
  • On a slow device, third-party execution competes directly with your own and the effect is multiplied (The Real Cost of JavaScript).
  • On a slow or filtered network, a vendor that fails to load must degrade gracefully — and a synchronous script that fails to load can stall the page entirely.
  • On a page handling credentials or payment, the same script that is an acceptable risk elsewhere is not.
What this costs
  • An iframe gives real isolation and costs integration: no shared styling, no direct DOM interaction, and communication only by explicit message passing.
  • A strict CSP is genuinely effective and genuinely ongoing work — every new vendor is a policy change, which is a feature disguised as friction.
  • SRI protects a fixed file and forbids the vendor from shipping fixes silently, which is exactly the trade you are making and should make deliberately.
  • Removing a vendor is usually a product conversation, not a technical one. The engineering contribution is making the cost visible.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThat an inline script tag carries the page's full authority follows from the origin model itself, so it holds in every browser and cannot be softened by configuration.
  • BROWSER-SPECIFICTracking protection, extension behaviour and third-party storage partitioning differ substantially — Safari and Firefox block or partition far more by default than Chromium does — so a widget that works in your browser may be silently degraded for a large share of real users (Storage Security and Durability).
  • SPEC-EVOLVINGThird-party cookie and storage partitioning is actively being tightened across engines, so any vendor whose functionality depends on cross-site state should be treated as being on a deprecation path rather than as a stable dependency.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

API Designapi-keys
Domains that do not exist yet
  • Testing & Reliability Engineering — a synthetic check that fails when an unexpected origin appears is the cheapest continuous inventory a team can have.
  • Software Design — a vendor integration is a dependency with an owner and a lifecycle, and treating it as a one-line snippet is what lets it outlive everyone who understood it.
OS & Networkingdns-resolution