Third-party scripts and supply chain
Every script you load from a vendor and every package you install runs with your page's full power, so you pick them carefully, pin and verify them, isolate what you can, and pay attention to what they cost in speed and privacy.
Code you did not write, running as you
Picture the checkout page of an online store. The team wrote the cart and the forms, reviews every pull request, and runs a strict CSP (Content Security Policy, the header that limits which scripts may run). Yet the page also loads an analytics library, a tag manager, an A/B testing tool, a chat widget, a fraud check, a session replay tool, and an ad pixel: seven companies whose code nobody on the team reviewed.
Then count the code inside the team's own bundle. A typical React app installs a few dozen packages from npm, which pull in hundreds or thousands of transitive dependencies (packages your packages depend on). Strangers wrote each one, and any of them can publish a new version tomorrow.
This is the supply chain of a frontend: all the code that reaches the user's browser without being written by you. The HTTP Archive's Web Almanac finds third-party requests on more than nine in ten pages it crawls.
Same origin, same power
A script tag does not run the vendor's code in the vendor's world. It runs it in yours. When you write script src="https://cdn.vendor.com/chat.js", the browser runs that file in your page with your origin (the scheme, host, and port of your page, such as https://shop.example). Once it runs, where the file came from no longer matters.
So the chat widget can do anything your own code can do:
- Read and change the DOM, including the card number field as the user types.
- Read cookies that are not
HttpOnly, pluslocalStorage,sessionStorage, and IndexedDB for your origin. - Send requests as the user to your own API, with the user's cookies attached, because the requests come from your origin.
- Send data anywhere with
fetch, an image URL, or a WebSocket, unless your CSP limits where the page may connect. - Load more scripts, which get the same power. One tag can become ten.
Two kinds of risk, and two kinds of cost
The risk has two shapes. In a compromised vendor, someone breaks into the company or CDN that serves a script and changes the file, so every site that loads it runs new code after its own deploy. In a compromised package, someone publishes a bad npm version, and it is built into your next deploy.
Even honest third-party code has a cost. It downloads bytes, opens connections, and uses the main thread, which hurts INP (Interaction to Next Paint, how long the page takes to show a response to a tap or key press). It often collects data about your users that you did not mean to share. This lesson covers attacks, defenses, speed, and privacy.
Check your understanding · Exercise 1
A marketing chat widget is loaded with a plain script tag from the vendor's CDN onto https://shop.example. What can the widget's code do? Select all that apply.
The mental model: a chain of people holding your keys
Think of your page as a house, and every piece of code that runs in it as a person holding a full set of keys. You hand keys to your own team, to each vendor whose script you load, and to each package author in your lockfile. Most of them are honest and careful. But a thief only needs one of them to lose their keys.
Worse, the people holding keys hand out copies. The chat vendor's script is built from its own npm packages and served from a CDN account that someone at the vendor logs into. Each link is a place where a change can slip in, and you see none of them.
Where you place trust, link by link
A file in the browser is only as trustworthy as every step that produced and delivered it. For a vendor tag:
- The vendor's developers and their dependencies. Their build pulls in packages, like yours.
- The vendor's build and release system. A CI server with a secret that can publish files.
- The hosting account and domain. Whoever controls
cdn.vendor.comdecides what that URL returns. Domains can be sold, expire, or be taken over. - The URL you wrote. A versioned URL (
/lib@4.4.1/lib.min.js) points at one file. A versionless URL (/latest/tag.js) points at whatever the vendor serves today. - Your page. The browser runs whatever arrives, unless you told it what to expect.
For an npm package, the last steps run on your side. Your installer picks a version, downloads it from the registry, may run its install scripts on your laptop or CI server, and your bundler copies its code into your own files. It is then served from your domain and looks first-party, but it is not.
Three questions for every link
Every defense in this lesson answers one of three questions:
| Question | What it means | Tools that answer it |
|---|---|---|
| Is it the code I expect? | The bytes that run are the bytes you reviewed or pinned. | SRI, lockfiles with hashes, npm ci, provenance, self-hosting |
| What can it reach if it goes bad? | Limit the blast radius: the damage a bad version can do. | Sandboxed iframes, workers, CSP, HttpOnly cookies, hosted payment fields |
| Do we need it at all? | Fewer links means fewer chances to fail. | Inventory, owners, reviews, budgets, removing dead tags |
No single tool answers all three. SRI proves a file did not change, not that the file you pinned was safe. A sandbox limits damage but cannot tell you the code is good. Removing a tag removes its risk entirely, but only for that tag, so real defense stacks all three.
Think first
Your bundle is served from https://app.example, your own domain. A package inside it turns out to be malicious. Does serving it from your own domain help?
No. Where a file is served from decides who can swap it after you deploy, but the code inside is whatever the package author wrote. Once bundled, a bad package runs with your origin, and a CSP that trusts your scripts trusts it too.
Self-hosting protects you from changes after release, such as a breached CDN or a sold domain. Against a bad version you installed, you need lockfiles, reviewed updates, and a delay before adopting new versions.
How real attacks happened
Supply-chain attacks are not theory. Each well-known case broke a different link in the chain, so read them as a map of entry points.
event-stream, 2018: a friendly new maintainer
event-stream was a popular Node.js streaming package with about two million downloads a week. Its original author no longer used it, so when a stranger offered to maintain it, he handed over publishing rights. The new maintainer added a dependency, flatmap-stream, whose code carried an encrypted payload.
The payload decrypted only inside the build of one app, the Copay Bitcoin wallet, where it tried to steal wallet keys. Everyone else got harmless code, so it went unnoticed for weeks. The lesson: a package can be safe in your app and dangerous in another, and a change of maintainer is a real risk signal.
British Airways, 2018: one edited library on the payment page
Attackers got into British Airways' systems and edited a copy of the Modernizr library that its website served. They added about 20 lines that copied card details from the payment form and sent them to a look-alike domain. The skimmer ran for about two weeks in August and September 2018.
Hundreds of thousands of customers were affected, and in 2020 the UK's data regulator fined the airline £20 million. A skimmer injected into checkout JavaScript is often called Magecart, after the groups that made it common; Ticketmaster UK was skimmed the same year through a chatbot vendor's script. The lessons: watch payment pages for script changes, and keep card fields out of your page.
ua-parser-js, 2021: a hijacked account
ua-parser-js parses user agent strings and had millions of weekly downloads. In October 2021 an attacker took over the maintainer's npm account and published three versions whose install script dropped a cryptominer and, on Windows, a password stealer. They were live for a few hours.
Anyone who installed fresh in those hours, without a lockfile or with a range that allowed the new version, got the malware on their machine or CI server. Teams with a committed lockfile and npm ci were safe until they chose to update.
Polyfill.io, 2024: the domain changed hands
cdn.polyfill.io served browser polyfills, picked per visitor from the User-Agent header, to hundreds of thousands of sites. In February 2024 the domain and project were sold to a new company, and the original author warned people to stop using it. In June 2024, researchers found it sending malicious code to some mobile visitors, redirecting them to scam and betting sites.
Nothing on the victim sites changed; their tag was the same as years before. The registrar suspended the domain, and Cloudflare and Fastly offered safe mirrors. Because the response varied per browser by design, SRI could not have pinned it. Self-hosting the polyfills, or dropping them because modern browsers need few, was the real fix.
chalk and debug, 2025: phishing a maintainer
On September 8, 2025, the maintainer of chalk, debug, and 16 other small, very common packages typed their login and two-factor code into a phishing site posing as npm support. The attacker published versions with code aimed at browsers: it hooked fetch and wallet APIs to swap cryptocurrency addresses. The bad versions were pulled within about two and a half hours, but the 18 packages had over 2 billion downloads a week combined.
| Incident | Link that broke | What would have helped |
|---|---|---|
| event-stream | Maintainer handover | Reviewing new dependencies in updates, fewer tiny packages |
| British Airways | Server hosting a first-party copy | Script change detection on payment pages, hosted card fields |
| ua-parser-js | Maintainer account | Lockfile, npm ci, no install scripts, waiting before updates |
| Polyfill.io | Domain ownership | Self-hosting, removing unneeded scripts, inventory |
| chalk and debug | Maintainer account (phished) | Lockfile, release-age delay, phishing-resistant 2FA for maintainers |
| Shai-Hulud worm (Sept 2025) | npm tokens stolen by install scripts, then used to publish more | No install scripts, release-age delay |
Check your understanding · Exercise 2
Which of these attacks would SRI on the script tag have stopped? Pick one.
Subresource Integrity: run only the file you reviewed
When you load a library from a CDN, you trust that the bytes at that URL will not change. Subresource Integrity (SRI) lets the browser check that trust instead of assuming it. You put a cryptographic hash of the exact file in the tag. The browser downloads the file, hashes it, and runs it only if the two hashes match.
A hash is a short fingerprint of a file. Change one byte and the fingerprint changes completely, and in practice nobody can craft a different file with the same SHA-384 fingerprint. So the hash is a promise: this tag runs exactly this file, or nothing.
crossorigin, the browser cannot read the bytes, so the check fails.What the browser does, step by step
- The parser finds a
script(orlink rel="stylesheet") with anintegrityattribute. - Because of
crossorigin="anonymous", it sends a CORS (Cross-Origin Resource Sharing) request without cookies. The CDN answers withAccess-Control-Allow-Origin: *, which lets your page read the bytes. - When the download finishes, the browser hashes the whole body with the named algorithm.
- It compares the result with the value in
integrity. If the attribute lists several hashes, it uses the strongest algorithm present and accepts the file if any hash for that algorithm matches. - On a match the script runs as usual. On a mismatch the browser does not run it, fires the element's
errorevent, and logs a console error. Listen for that event if you want to log the failure or load a fallback.
Note step 5: SRI fails closed. A changed file is not run, which is right for security, but the feature that needed it is now broken. A page that cannot live without a CDN library should fall back to a self-hosted copy, or just self-host.
Making the hash
# Hash the exact file you reviewed, never a file fetched at build time
# from a URL you do not control.
openssl dgst -sha384 -binary chart.min.js | openssl base64 -A
# k9Vh...Qe2X (64 base64 characters for SHA-384)
# The tag then carries it with the algorithm prefix:
# integrity="sha384-k9Vh...Qe2X"
sha256 and sha512 also work.Bundlers can add hashes to your own chunks on a separate CDN host; webpack has the webpack-subresource-integrity plugin. Public CDNs such as cdnjs and jsDelivr show a ready-made SRI tag for each pinned file.
Why crossorigin matters
SRI must read the bytes to hash them, and a page may read a cross-origin response only if the server allows it with CORS. So a cross-origin file with integrity must be fetched in CORS mode. Forget crossorigin="anonymous" and the check fails even if the file is perfect. Use anonymous (no cookies) unless the file truly needs credentials.
Where SRI cannot help
SRI needs a file that never changes. Many third-party scripts are built to change:
- Versionless loaders. Analytics tags, tag managers, and ad scripts live at one URL that the vendor updates in place. Pin a hash and the next vendor update breaks your site.
- Responses that vary per visitor. Polyfill.io returned different code per browser. Google Fonts CSS returns different
@font-facerules per browser. One hash cannot match them all. - Scripts loaded by other scripts. If a pinned loader adds more scripts, those have no
integrityunless the loader sets it. Most loaders do not. - Code bundled from npm. SRI checks delivery, not content. A malicious package inside your bundle is hashed along with the rest.
For versionless vendor tags you are trusting the vendor. You can self-host a pinned copy (if their terms allow it), isolate the tag, watch it for changes, or accept the risk knowingly. SRI is the right tool for pinned libraries, fonts, and stylesheets on CDNs, and for your own assets on a separate host.
Check your understanding · Exercise 3
You added integrity to script src="https://cdn.other.example/lib@2.1.0/lib.js" and now the script never runs, even though the file is correct. What could be wrong? Select all that apply.
Your npm packages are third-party code too
Most code in your own bundle came from npm. A mid-size React app often has more than a thousand packages in its lockfile. Their code runs in the browser with your origin, and their install scripts run on your laptops and CI servers, which hold secrets such as deploy keys and npm tokens.
Lockfiles: the same bytes every time
package.json usually lists ranges, not versions: "debug": "^4.3.4" means any 4.x at or above 4.3.4. Two installs a day apart can get different code. A lockfile (package-lock.json, pnpm-lock.yaml, or yarn.lock) records the exact version picked for every package in the tree, where it was downloaded from, and an integrity hash of the tarball.
package.json says what you would accept; the lockfile says what you actually ship. npm ci refuses to install anything else.- Commit the lockfile. It is the list of code you actually ship. Changes to it should show up in review like code changes.
- Install with
npm ciin CI (orpnpm install --frozen-lockfile). It installs exactly what the lockfile says, checks every tarball hash, and fails ifpackage.jsonand the lockfile disagree. Plainnpm installmay update the lockfile. - Read lockfile diffs. A small feature should not add 40 new packages. A new package from an unknown author, or a resolved URL pointing at a strange host, deserves a question.
A lockfile is why ua-parser-js and chalk missed many teams, but it does not protect whoever runs an update during the bad hours.
Updating on purpose, a little late
You still have to update, because old versions collect known bugs. A bot such as Dependabot or Renovate opens one small pull request per update, so each change is reviewed and tested. Most malicious versions are pulled within hours or days, so a short delay before adopting new releases removes most of the risk. Renovate and pnpm (since 10.16) both have a minimumReleaseAge setting for this.
Auditing: known holes and fake signals
npm audit compares your lockfile with the GitHub Advisory Database of known vulnerabilities and lists affected packages by severity. Run it, or a similar scanner, in CI, and read the results with care: many findings are in dev-only tools that never reach the browser. Separate "ships to users" (npm audit --omit=dev) from "runs on our machines".
An audit cannot see fresh malware nobody has reported yet. The other controls in this chapter catch those.
Install scripts and provenance
npm runs a package's preinstall, install, and postinstall scripts by default, with your user's rights. That is how many attacks steal tokens from laptops and CI. pnpm 10 and later skips dependencies' install scripts unless you list them as allowed. With npm, set ignore-scripts=true in .npmrc and run the few needed scripts by hand.
Provenance answers "was this tarball built from the source it claims?". Packages published from a supported CI (such as GitHub Actions) can carry a signed statement linking the tarball to a commit and a build, recorded in a public log (Sigstore). npm audit signatures checks registry signatures and provenance for your installed packages. Provenance does not prove the code is safe, but a new version that lacks it when earlier ones had it suggests a publish from a stolen token.
Typosquatting and dependency confusion
Some attacks publish a fake package and wait for a mistake. Typosquatting uses names one typo away from popular ones: in 2017, crossenv (the real one is cross-env) worked like the original and also sent the developer's environment variables to the attacker. Attackers also register names that AI coding tools tend to invent, packages that sound right but never existed.
Dependency confusion targets private packages. If your app depends on an internal name like acme-ui-utils, and someone publishes a public package with that name and a higher version, a badly configured installer may pick the public one. A researcher showed this in 2021 against dozens of large companies. The fix is a scope you own (@acme/ui-utils) mapped to your private registry in .npmrc.
| Control | Stops | Does not stop |
|---|---|---|
Lockfile and npm ci | Surprise new versions in builds | A bad version you chose to update to |
| Release-age delay | Most short-lived malicious releases | A long-hidden payload like event-stream |
npm audit | Known vulnerable versions | Unknown, fresh malware |
| No install scripts | Malware that runs at install time | Malware that runs in the browser |
| Provenance checks | Some publishes from stolen tokens | Bad code shipped through the real CI |
| Scoped private names | Dependency confusion | Typos in public package names |
Check your understanding · Exercise 4
A teammate runs npm install on a fresh clone and the lockfile changes. Which statements are true? Select all that apply.
Isolating code you cannot fully trust
Integrity checks ask "is it the file I expect?". Isolation asks a different question: "if this file goes bad, what can it reach?". For a plain script tag the answer is everything; isolation moves each vendor somewhere the answer is much smaller.
Sandboxed iframes on another origin
The strongest boundary the browser offers is the origin. Code in an iframe from https://widget.vendor.com cannot read your DOM, cookies, or storage, and you cannot read its; the two sides talk only through postMessage, which carries data, not power. The sandbox attribute also limits what the frame may do, such as submit forms, open pop-ups, or navigate the top page. The clickjacking and iframes lesson covers sandbox and safe postMessage handling.
This is how payment providers keep card numbers off your page. Stripe Elements, Adyen, and Braintree render each card field in their own iframe, so the digits are typed into the provider's origin and a skimmer on your page sees only an iframe. It also shrinks your scope under PCI DSS, the card industry's security standard.
The cost: an iframe is a separate document with its own memory and requests, and sizing it to its content takes messages back and forth. It only fits widgets that live in a box, such as chat, video, payment fields, and social embeds. An analytics script that must watch your clicks cannot.
Web workers and Partytown
A web worker runs JavaScript on a separate thread with no DOM access. Partytown, an open-source library first built at Builder.io, runs tag scripts in a worker and forwards their DOM and window calls to the main thread through a proxy. That frees the main thread, which helps INP, while scripts written for the page mostly still work.
Two caveats. Partytown is a performance tool, not a security boundary: the worker is on your origin, and the proxy reaches your page by design. And scripts that need synchronous layout reads, many events, or tight timing may break; the project itself warns it will not work in every case. Test each script you move and keep a way back (Next.js offers this as the experimental worker strategy of its Script component).
Moving tags to the server
Server-side tagging sends one event stream from your page to an endpoint you control (for example a Google Tag Manager server container on your own subdomain), and that server forwards data to each vendor. The vendors' code never runs in the browser, you decide which fields each one gets, and the page loads one small script instead of five. The cost is a server to run and pay for, and vendors whose features need browser code (session replay, chat, on-page tests) cannot move.
Decision Where should a vendor's code run?
What we pick: Payment fields, chat, and embeds belong in cross-origin iframes. Analytics and ad conversions move to server-side forwarding when the team can run it. What must stay as a script tag gets a small blast radius: HttpOnly cookies, a strict CSP, and no access to pages that handle cards or passwords.
Check your understanding · Exercise 5
Which statements about isolating vendor code are true? Select all that apply.
Tag managers and governance
A tag manager such as Google Tag Manager (GTM) or Tealium is a script you load once that then loads other scripts, based on rules set in a web dashboard. Marketing can add a new pixel on Friday afternoon without a deploy. That is the whole point, and also the risk.
A deploy pipeline outside your deploy pipeline
A custom HTML tag in a tag manager can contain any script. Anyone with publish rights in the container can put code on every page of your site, with your origin's power, no review, no tests, and no entry in your git history. If one of those accounts is phished, the attacker has the same reach.
Tag managers also get around some defenses by design. Their loader is versionless, so SRI does not fit. Under a nonce-based CSP with 'strict-dynamic', the tag manager snippet carries the nonce and every script it adds is trusted too (see the CSP lesson). That is the right policy, but CSP will not stop a bad tag published in the dashboard, so your controls here are about people and process.
Rules that work
- Few publishers. Many people can edit; two or three can publish. Turn on two-step login for every account.
- Allow and block lists. GTM reads
gtm.allowlistandgtm.blocklistfrom the data layer in your page code. Block the custom HTML type there and no dashboard user can turn it back on. - An inventory. Every tag with its purpose, owning team, pages, data received, and last review date. A tag with no owner is a tag to remove.
- Page scoping. Fire tags only on the pages that need them. No marketing tags on checkout, login, or account pages.
- Change alerts. Alert on every container publish and review the diff like a pull request.
- A kill switch. Know how to remove a tag, or the whole container, in minutes, without a full release.
Payment pages have rules of their own
The skimming attacks above led the card industry to add script rules to PCI DSS 4.0 (now 4.0.1), mandatory since March 31, 2025. For scripts on payment pages, requirement 6.4.3 asks for an inventory with a written reason for each script, a way to confirm each is authorized, and a way to assure its integrity. Requirement 11.6.1 asks for a mechanism that alerts on unauthorized changes to those scripts and to security-relevant HTTP headers, as the browser receives them.
Teams usually meet these with hosted payment fields, a short script list, a strict CSP with reporting, and a monitoring service that loads the page and compares its scripts. The same habits suit any page with passwords or personal data: know every script, why it is there, and when it changes.
Check your understanding · Exercise 6
Your site uses a nonce-based CSP with 'strict-dynamic', and Google Tag Manager carries the nonce. A phished marketing account publishes a custom HTML tag with a skimmer. What happens? Select all that apply.
The performance and privacy cost
Third-party scripts are among the most common causes of slow pages, and often the biggest privacy risk on a site. Both costs are easy to miss, because each tag looks small on its own.
Main-thread time is the real price
The bigger cost is usually main-thread time, not bytes: the milliseconds spent parsing, compiling, and running a script on the thread that also handles taps and paints frames. Any piece of work over 50 ms is a long task, and a tap that lands during one waits for it, which shows up directly in INP. On a mid-range phone, the same script can take several times longer than on a developer laptop.
A few patterns make it worse:
- Render-blocking tags. A classic
scriptin the head withoutasyncordeferstops HTML parsing until it downloads and runs. Client-side A/B testing tools often ask for this, plus a snippet that hides the page until the test loads, which delays LCP (Largest Contentful Paint) for every visitor. - Work on every event. Session replay and some analytics tools listen to scroll, input, and DOM changes, so they cost something on every interaction.
- New connections. Each new domain needs DNS, TCP, and TLS before the first byte, often 100 to 300 ms on mobile. Ten tags on ten domains is ten handshakes.
- Layout shifts. Ads, consent banners, and embeds that appear without reserved space push content down and hurt CLS (Cumulative Layout Shift).
Measuring what each vendor costs
Measure the cost per vendor. Lighthouse groups main-thread time and bytes by third-party entity, and the Chrome DevTools Performance panel shows which third party owns each task. In the field, the Long Animation Frames API (PerformanceObserver with type long-animation-frame, Chromium only) reports the scripts in each slow frame with their source URL, so your real-user monitoring can blame a specific vendor. A simple test that always works: block the vendor's domain in DevTools and compare.
Then give third parties a budget, as the performance budgets lesson describes: for example, at most 150 KB of compressed third-party JavaScript and 200 ms of main-thread time on a mid-range phone. A new tag must fit, or something else must go.
The privacy cost
A script on your page sees what your users see. Analytics and ad scripts usually send the page URL, referrer, and device details to the vendor, so search terms, emails, or IDs in your URLs leave too. Session replay tools record what users type unless fields are masked, and researchers have found them capturing passwords on real sites. In 2022, The Markup reported that the Meta Pixel on many US hospital sites sent details of appointment bookings to Facebook.
- Cookies. Third-party cookies let a vendor follow one user across every site that loads its tag. Safari blocks them and Firefox partitions them per site by default. Chrome still allows them by default, after Google dropped its plan to remove them. Do not build on them, and do not assume they are gone.
- Fingerprinting. Some scripts build an identifier from the browser's traits (fonts, screen, GPU, canvas output) to track users who block cookies. It is hard to detect and often breaks browser policies.
- Law and consent. In the EU and UK, most non-essential tracking needs consent before it runs (GDPR and the ePrivacy rules). A banner that does not actually hold tags back until the user decides is not compliance.
The frontend controls are concrete: keep personal data out of URLs, set Referrer-Policy: strict-origin-when-cross-origin (the browser default today) or stricter, mask inputs in session replay by default, load tags only after consent, and send the minimum fields through server-side forwarding when you can.
Think first
A client-side A/B testing tool asks you to put its script, synchronously, at the top of the head, plus a snippet that hides the page for up to 4 seconds. Why does it ask, and what would you push back with?
It wants to change the page before the user sees it. If it loads late, users see the original and then a jump to the variant, which hurts both the test and CLS.
But every visitor waits for a third-party download before seeing anything, and a slow vendor delays LCP by up to the hide timeout. Push for server-side or edge assignment: pick the variant on the server, render it directly, and send exposure events afterwards. Failing that, cap the hide timeout low, limit the tool to the pages under test, and measure LCP with and without it.
Loading third-party code without hurting the page
Once a tag has earned its place, how you load it decides most of its speed cost. The loading strategies lesson covers the general tools; here they are for vendor code, from least to most deferred.
async, defer, and later
| Strategy | When it runs | Good for |
|---|---|---|
Plain script in head | Blocks parsing until downloaded and run | Almost nothing from a third party |
async | As soon as it downloads, in any order, possibly before the page is parsed | Independent tags like analytics loaders |
defer | After parsing, in document order, before DOMContentLoaded | Scripts that need the DOM or each other |
| After load or idle | Injected after the load event or in requestIdleCallback | Chat, feedback, and survey widgets |
| On interaction | Injected when the user clicks or focuses the widget | Video, maps, chat, social embeds |
Framework helpers wrap these choices: the Next.js Script component has beforeInteractive, afterInteractive (the default), and lazyOnload strategies. Default a new tag to lazyOnload unless someone can explain why it must run earlier. For a tag needed early, preconnect to its domain to start the handshake sooner, but only for one or two domains, because each preconnect costs work too.
Facades: load the heavy thing on click
A facade is a light stand-in that looks like the real widget. A YouTube embed is the classic case: the real iframe pulls in several hundred kilobytes of JavaScript even if nobody presses play. A facade shows the thumbnail and a play button, built from one image and a little of your own code, and swaps in the real player on click. Most visitors never click, so most never pay.
The same idea works for chat (a button that loads the chat script on click), maps (a static image), and social embeds. The facade must be a real button with an accessible name such as "Play video: Product tour", and the click should move focus into the loaded widget so keyboard and screen reader users are not lost. Privacy improves too: the vendor is not contacted until the user chooses, and YouTube also offers a youtube-nocookie.com domain for embeds.
Self-hosting or the vendor's CDN
A library such as a chart package or a font can come from a public CDN or from your own domain. The old argument for public CDNs was a shared cache: a visitor who loaded jQuery from a CDN on one site would already have it on yours. That argument is gone, because browsers now partition the HTTP cache by top-level site: Safari since 2013, Chrome since version 86 (2020), and Firefox since version 85 (2021).
Decision Should this library come from a public CDN or your own domain?
What we pick: Self-host libraries and fonts by default. Use a versioned public CDN with SRI only when you cannot bundle. Accept versionless vendor URLs only for services that require them, and treat those as the tags to isolate, scope, and monitor.
Check your understanding · Exercise 7
A product page embeds a YouTube video below the fold and a support chat. Which changes reduce cost without losing the features? Select all that apply.
Using it in a system design interview
Any design with checkout, login, analytics, ads, embeds, or a marketing site has third-party code. You rarely get a whole question on it, but you get credit for raising it at the right moment in a sentence or two, and going deeper if asked.
When to bring it up
- Payments or login in scope. Say card fields live in the provider's iframe, and these pages load almost no third-party scripts.
- Analytics or experiments in the requirements. Mention the tag budget, late loading, and server-side A/B assignment.
- Embeds (video, maps, chat). Mention facades.
- Performance targets. Add that third-party main-thread time counts toward LCP and INP goals and is measured per vendor.
- A design system or shared library. Mention lockfiles, reviewed updates, and provenance for what you publish.
What to draw
One box diagram covers it. Draw the page with your bundle inside, and next to it the vendor boxes, each with an arrow showing how it gets in (script tag, iframe, or server-side forwarder) and which pages it may run on. Then label the controls on the arrows: SRI on the pinned CDN library, nonce and 'strict-dynamic' on the loader, sandbox and postMessage on the iframe, and a consent gate on analytics.
How deep to go by level
| Level | What a strong answer shows |
|---|---|
| Junior | Third-party scripts run with full page access; use async or defer; keep tags off checkout; know what SRI checks. |
| Mid-level | Lockfiles and npm ci, SRI with crossorigin, facades, hosted payment fields, measuring main-thread time per vendor. |
| Senior | Integrity versus isolation versus inventory; why SRI fails for versionless tags; CSP with 'strict-dynamic' trusts tag managers; server-side tagging; release-age delays and provenance; consent gating. |
| Staff | Governance as a system: tag inventory with owners, publish rights, change detection on payment pages, a third-party budget in CI, a kill switch, and an incident runbook for a compromised vendor. |
If asked: a vendor is compromised right now
- Remove or block the tag: tag manager pause, feature flag, or a CSP change that drops its host. Minutes matter.
- Find the blast radius: which pages loaded it, since when, and what those pages hold (cards, passwords, tokens).
- Rotate what could have leaked: sessions, API keys exposed to the browser, and any tokens readable by script.
- Tell the people who need to know: security, legal, and, if personal or card data was exposed, the regulators and users as the law requires.
- Fix the gap: pin, self-host, isolate, or drop the vendor, and add the change detection that would have caught it sooner.
Check your understanding · Exercise 8
You are designing the checkout flow of a large store. Which choices would you say out loud? Select all that apply.
Summary
- Every third-party script and every npm package runs with your origin's full power. Treat it like code merged without review.
- Think in three questions: is it the code I expect (integrity), what can it reach if bad (isolation), and do we need it (inventory).
- SRI pins a file by hash and needs
crossorigin. It fits versioned libraries, not versionless tags or code already in your bundle. - Lockfiles with
npm ci, small reviewed updates, a release-age delay, no install scripts, and scoped private names stop most npm attacks. - Put payment fields, chat, and embeds in cross-origin iframes; move data collection server-side; keep high-value pages nearly tag-free.
- A tag manager is an unreviewed deploy path: limit publishers, restrict tag types, alert on publishes, and keep a kill switch.
- Budget third-party main-thread time per vendor, load tags late or on interaction, and use facades for heavy embeds.
- Third-party tags see what users see. Keep personal data out of URLs, mask session replay, and run tags only after consent.
In the interview
Questions interviewers ask about this. Answer each out loud first, then compare.
Question 1
Why is a third-party script on your page a security risk even if the vendor is honest?
Because the browser runs it with your origin's full power: it can read the DOM, non-HttpOnly cookies and storage, call your API as the user, send data anywhere the CSP allows, and load more scripts. Honesty is not the issue; the vendor's accounts, build system, CDN, and domain are all ways in for an attacker.
Real cases broke each link: a maintainer handover (event-stream), a hijacked or phished npm account (ua-parser-js, chalk), a sold domain (Polyfill.io), and a breached server on a payment page (British Airways). So I think about integrity, isolation, and whether we need the script at all.
Question 2
What does Subresource Integrity do, and when can you not use it?
SRI puts a hash of the expected file in the tag's integrity attribute. The browser hashes the downloaded bytes and refuses to run the file on a mismatch. Cross-origin files need crossorigin="anonymous" and a CORS header from the CDN, or the check fails.
It only works for files that never change: a versioned library on a CDN, or your own assets on a separate host. It cannot cover versionless vendor tags that update in place, responses that vary per browser, scripts added by other scripts, or malicious code already bundled from npm.
Question 3
How do you protect a frontend build from a malicious npm release?
Commit the lockfile and install with npm ci so builds use reviewed versions with checked hashes. Update through small bot pull requests and wait a few days before adopting brand-new releases. Turn off install scripts by default, run npm audit for known issues, check provenance with npm audit signatures, and use a company scope for private packages to stop dependency confusion.
None of these proves code is safe, so I also keep the dependency tree small and review lockfile diffs for unexpected new packages.
Question 4
Marketing wants five new tags through the tag manager. How do you keep the site fast and safe?
Each tag needs an owner, a purpose, the pages it runs on, and the data it receives, recorded in an inventory. Tags must fit a budget for bytes and main-thread time, measured per vendor, and load after the page is usable or on interaction. No tags on checkout or login.
Publishing is limited to a few people with two-step login, the container's allowed tag types are restricted from page code, publishes trigger alerts, and there is a kill switch. Where possible, data collection moves to server-side tagging so vendor code does not run in the browser at all.
Question 5
Does a strict CSP protect you from a compromised third-party script?
Only partly. A nonce-based CSP with 'strict-dynamic' trusts your loader and everything it adds, so a compromised vendor script or tag runs. A host allow list trusts every file on the vendor's domain, so a changed file there runs too.
CSP still helps: it stops injected scripts that the vendor did not load, limits connect-src for exfiltration, and reports surprises. For the vendor's own code you need SRI or self-hosting for integrity, and iframes or server-side forwarding for isolation.
Practice it
These design problems lean on third-party scripts and supply chain.
- Design a cookie consent managerEasy
- Design a customer support chat widgetMedium
- Design an embeddable poll widgetMedium
- Design a client-side analytics SDKMedium
- Design a frontend logging and error SDKMedium
- Design a plan to make a slow e-commerce site fastMedium
- Design a checkout flowHard
- Design a secure payment formHard
- Design an embeddable third-party widgetHard
- Design an online code playground like CodePenHard
- Design a browser extensionHard
References
- MDN: Subresource Integrity How the
integrityattribute works, with hash examples. - W3C: Subresource Integrity The specification, including how multiple hashes are chosen.
- web.dev: Efficiently load third-party JavaScript Loading strategies, preconnect, and facades.
- Chrome for Developers: Lazy load third-party resources with facades The Lighthouse audit and example facades.
- npm Docs: npm ci Clean, lockfile-exact installs for CI.
- npm Docs: Generating provenance statements How provenance links a package to its source and build.
- pnpm: Mitigating supply chain attacks Blocked install scripts, an allow list for builds, and
minimumReleaseAge. - Google Tag Manager: Restrict tag deployment The
gtm.allowlistandgtm.blocklistsettings in page code. - Partytown documentation How the worker proxy works and its known trade-offs.
Read next
- Content Security PolicyA response header that tells the browser which scripts, frames, and connections a page may use, so an injected script is blocked even when an XSS bug slips through.
- Cross-site scripting (XSS)Cross-site scripting is when text from outside your app ends up running as code inside your page, with every power your own code has: the most common and most damaging frontend security bug, and the one frontend engineers own most directly.
- Clickjacking and iframesIframes power embeds, payment fields, and widgets, and also let an attacker hide your page under a fake one to steal a click: control both directions with framing headers, the sandbox attribute, and checked postMessage.
- Loading strategiesDeciding what the browser loads first, what it loads later, and what it loads only when needed, so the pixels that matter show up fast.
- Runtime performanceKeeping the page responsive after it loads: finding long tasks on the main thread, splitting or moving that work, and avoiding needless re-renders so every click and keypress paints in under 200 ms.
- Performance budgets and monitoringAgreed limits on what a page may cost (kilobytes of JavaScript, LCP, INP) that CI checks on every pull request and real-user monitoring watches in production, so speed stops getting worse one small change at a time.