Skip to content
academia.sh

Lesson 19 / 27

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.

Contents

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.

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.

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close