---
title: 'Third-Party Script Risk'
source: 'https://academia.sh/en/courses/frontend-quality/third-party-script-risk'
course: 'Frontend Quality'
language: en
updated: '2026-08-17T18:11:06+00:00'
license: 'CC BY-SA 4.0'
---

# Third-Party Script Risk

Code that was not written in-house running with the page's own origin authority; the links of the supply chain, generating and validating a subresource integrity hash, what the integrity check does not cover, and runtime isolation.

The previous lessons covered the application's own code and its own server's
configuration. The North Slope Measurement Station interface also runs code it did not
write: the external script that draws the measurement chart, the map frame embedded in the
page, and the dependency tree bundled at build time.

This lesson builds the risk that code carries and the layers that narrow it. Its starting
point is a single observation: a third-party script running on the page is no less
privileged than the application's own script.

## Inherited Authority

The concept of authority built in the first lesson applies again here. Code brought into
the page with a script element runs inside the document's origin. The same-origin policy
does not treat it as foreign: it can read and change the entire document tree, reach
cookies that are not marked `HttpOnly`, read local storage, and send requests with the
user's session. It can see what is typed into the measurement note field before the user
even presses submit.

This transfer of authority is not a defect; it is what a script element is. The defect is
granting that authority without being aware of it. The sentence "a script that draws
charts" does not say the script will only draw charts; it says only that it behaves that
way right now.

The chart script may be well-intentioned on its own. The risk lies in the assumption that
its content today will stay the same tomorrow, and that assumption has to be checked
separately at every link in the chain.

## Links in the Supply Chain

Working backward to the source of code running on the page reveals several distinct trust
points.

**Direct dependencies** are packages the developer knowingly added; their number stays at
a reviewable scale. **Transitive dependencies** are those packages' own dependencies, and
their number grows fast. Most of the code that ends up in a production bundle usually
comes from this second set and has never been read line by line.

The **package registry** is where versions get distributed. Publishing a version is
something anyone with access to the maintainer's account can do. **Build tools** come with
their own plugins, and code that runs at build time determines what ends up in the
production bundle.

Scripts loaded at runtime through a **content delivery network** are the most fragile link
in the chain: the code never lives in the application's own repository, it can differ on
every page load if the version is not pinned, and a change never shows up in any build
record.

The length of the chain produces a rule: for every link, there has to be an answer to "what
tells us when this content changes." The mechanism below answers that question for the
runtime link.

## Subresource Integrity

**Subresource integrity** gives a script or stylesheet element a way to declare, ahead of
time, the digest of the content it is about to load. The browser fetches the content,
computes its digest, and compares it against the declared value; if they do not match, it
does not use the resource.

```js
// integrity.mjs — generates and validates a subresource integrity hash
import { createHash } from 'node:crypto';

const SCRIPT_ORIGINAL = 'export function measurementChart(data) { return data.map((n) => n.temperature); }\n';
const SCRIPT_CHANGED = 'export function measurementChart(data) { return data.map((n) => n.temperature ?? 0); }\n';

function integrityValue(content, algorithm = 'sha384') {
  const hash = createHash(algorithm).update(content, 'utf8').digest('base64');
  return `${algorithm}-${hash}`;
}

function validate(content, expectedList) {
  const list = expectedList.trim().split(/\s+/);
  for (const expected of list) {
    const [algorithm] = expected.split('-');
    if (integrityValue(content, algorithm) === expected) return { valid: true, matched: expected };
  }
  return { valid: false, matched: null };
}

const declared = `${integrityValue(SCRIPT_ORIGINAL)} ${integrityValue(SCRIPT_ORIGINAL, 'sha512')}`;
console.log('declared integrity value:');
console.log(' ', declared.split(' ')[0]);
console.log(' ', declared.split(' ')[1]);

console.log('\ndelivered content evaluation:');
for (const [name, content] of [['published version', SCRIPT_ORIGINAL], ['changed version', SCRIPT_CHANGED]]) {
  const result = validate(content, declared);
  console.log(' ', name.padEnd(22), result.valid ? 'loaded' : 'rejected');
}

console.log('\nhash comparison (sha384):');
console.log(' ', 'published'.padEnd(14), integrityValue(SCRIPT_ORIGINAL).slice(0, 34) + '...');
console.log(' ', 'changed'.padEnd(14), integrityValue(SCRIPT_CHANGED).slice(0, 34) + '...');
```

```
$ node integrity.mjs
declared integrity value:
  sha384-LVWTQyA7w7fDnBfx1c0H+qcKSnAFNkBgJUkohgl42LsIDnFIixQReAGIJ77hm49H
  sha512-/BpUuICYtzAvDjEMyZ20SfGMdtz4aOEPfsR3lJ8ZM+1H9nJN1ZvbUX1X+mvzrFaipIwbq0DQ3/iQK8mKZAlg+Q==

delivered content evaluation:
  published version      loaded
  changed version        rejected

hash comparison (sha384):
  published      sha384-LVWTQyA7w7fDnBfx1c0H+qcKSnA...
  changed        sha384-SpoaUzkpz3NBC1t/wDYp21IsQ2Q...
```

The difference between the two versions is a single operator; the hashes do not even
share a common prefix. This behavior of the digest function carries the whole mechanism: a
single character changing in the content makes the hash unrecognizable, and validation
fails.

The declared value can carry more than one hash. The browser picks the strongest algorithm
it supports; if one algorithm is ever considered untrustworthy in the future, the list
allows a transition without changing the element.

Two implementation rules are binding. First, an element loaded cross-origin needs the
`crossorigin` attribute; an integrity check cannot be performed on an opaque response whose
content cannot be read. Second, for the hash to mean anything, the address has to be
versioned. An address that means "the latest version" breaks validation the moment the
provider updates the content; that break is not a defect, it is proof the mechanism is
working — but if the application is expected to keep working, the address has to be fixed.

## What Integrity Does Not Cover

The integrity check validates that the loaded bytes are the expected bytes. It does not
validate what those bytes do. The provider publishes a new version, the application
declares the new hash, and if the new version does something the first version did not do,
the check raises no warning at all.

The check also only covers the declared element. If the loaded script itself fetches other
scripts at runtime, those fetches are not bound to this element. The content security
policy's source lists close part of this gap: integrity fixes "what will come from this
address," while the policy limits "which addresses loading may happen from." The two
mechanisms do not substitute for each other.

## Code Added at Runtime

One class of tool separates adding a script to the page from a code change: pieces of code
defined through a management console are brought onto the page at runtime by the
application's loader. On the measurement station, a usage-tracking script added this way
never appears in the application's repository at all.

This arrangement has two security consequences. First, the set of code running on the page
can no longer be read from the repository; the inventory can only be extracted from the
running page. Second, the authority to add code to the page has been opened up to a group
that does not go through code review, and that group's account security becomes part of
the application's security.

There are three ways to narrow this. The content security policy's source lists limit the
addresses the loader can fetch from; this does not stop the tool from being a load point,
but it stops it from being an unbounded one. Violation reports collected in report-only
mode make an unexpected load point visible. The third is administrative: the list of
third-party code running on the page is kept in writing, each entry matched to an owner and
a justification, and an entry with no remaining justification is removed. On a page with no
inventory, none of this lesson's measures count as complete.

## Isolation and Build-Time Measures

If a third-party component needs to show interface on the page, using a frame loaded from
a separate origin instead of a script element cuts off the transfer of authority. The
**sandboxing** attributes introduced in the Web Fundamentals and HTML course list which
capabilities the frame may use; the permission policy header narrows device access.
Communication is set up over a narrow message interface between the main document and the
frame, and incoming messages are validated against the origin they came from. The map
frame is a typical example of this structure: there is no reason for the map to read
measurement notes, and the frame blocks that structurally.

On the build-time side, four measures work together. The **lockfile** writes resolved
versions and content digests into the repository; a setup that ignores the lockfile
disables this entire defense. **Version pinning** ensures the next install fetches the same
code. **Vulnerability scanning** matches published vulnerability records against the
installed set. **Measuring bundle content** makes the size and composition of code entering
the production bundle visible; a dependency growing unexpectedly is often the first signal.

One last measure is administrative: adding a new dependency is a code change and goes
through the same review. The choice between writing a function in-house and adding a
package is not a line-count comparison; it is a trust-surface decision.

## Summary

- Third-party code brought into the page with a script element inherits every authority
  the application's origin has; its access to the document tree, cookies, and the network
  is no different from the application's own code.
- The supply chain consists of direct and transitive dependencies, the registry, build
  tools, and the delivery network; every link needs a mechanism that reports when it
  changes.
- Subresource integrity declares ahead of time the digest of content to be loaded; a
  single-character change makes the hash unrecognizable and the resource unusable.
- Integrity validates bytes, not behavior, and covers only the declared element; it is used
  together with the policy's source lists.
- A third-party component that shows interface is moved to an isolated frame loaded from a
  separate origin; on the build side, the lockfile, version pinning, vulnerability
  scanning, and bundle measurement work together.

## Next Step

The six lessons in this topic focused on specific mechanisms: escaping, policy, sharing
headers, request tokens, security headers, and integrity. Most vulnerabilities encountered
in practice fall into the classes these mechanisms cover, but not all of them do. The next
lesson takes up common vulnerability classes through their client-side projection:
authorization decisions made in the interface, address redirects, and how much trust to
place in data reaching the client.
