BuilderProof editorial team22 min read6 views

Who Can Change the Script After You Ship It? A Proposed Axis for Third-Party Script Integrity (September 2026)

A script element without integrity metadata does not request a specific script. It requests whatever the host at that address is serving when a visitor arrives. A pre-registration for a third-party script integrity axis, with the two specifications that make the half-fix fail harder than the no-fix.

Updated on September 25, 2026

Flat diagram on a cream background: a tall navy rounded-rectangle outline on the left, with a thin navy line running right from its edge and stopping just short of two solid amber squares with rounded corners, stacked one above the other with a gap between them.
Flat diagram on a cream background: a tall navy rounded-rectangle outline on the left, with a thin navy line running right from its edge and stopping just short of two solid amber squares with rounded corners, stacked one above the other with a gap between them.
On this page

Quick answer

An AI app builder can hand you a project whose delivered HTML executes code fetched from hosts you do not control, and in almost every case that code is identified by nothing but a URL. BuilderProof proposes Third-Party Script Integrity as a benchmark axis: does the generated application pin externally hosted executable code to a cryptographic hash, is that pinning enforced by policy rather than left to whoever wrote each tag, and is the class of script that genuinely cannot be pinned named rather than quietly ignored. This is a pre-registration. No builder is scored here, no placement is published, and no first-party measurement is claimed. The axis is written against two primary specifications and one framework reference, all read in full on 25 September 2026.

The trap it is named after is the same-URL illusion: a URL is a stable identifier, so it feels like a stable referent. The W3C specification says exactly this in its own goals section, and says it better than we would.

W3C

Why third-party script integrity belongs on the benchmark

The Subresource Integrity specification is unusually direct about the problem it exists to solve. Its introduction states that "an attacker who can replace the file on the Content Delivery Network (CDN) server has the ability to inject arbitrary content." Its goals section then states what the mechanism buys you, and the phrasing is the whole axis in one sentence:

Subresource Integrity, Editor's Draft, 20 March 2026, section 1.1: "Compromise of a third-party service should not automatically mean compromise of every site which includes its scripts. Content authors will have a mechanism by which they can specify expectations for content they load, meaning for example that they could load a specific script, and not any script that happens to have a particular URL."

That last clause is the failure mode stated as a design goal. Without integrity metadata, a script element does not request a specific script. It requests whatever the host at that address is serving at the moment a visitor arrives. Every test a developer can run confirms the URL is correct, because the URL is correct. The URL was never the thing at risk.

This matters for generated applications specifically because the developer did not write the tag. A builder that wires up an analytics snippet, a font loader, a chart library or a payments SDK has made a decision about executable code on the developer's behalf, and the developer's first encounter with that decision is usually a working page.

MDN

The half-fix that breaks the page, and it breaks in the direction nobody expects

The obvious next step after reading the specification is to paste an integrity attribute onto an external script and move on. That does not degrade gracefully into "unverified". It fails hard.

Cross-origin requests carrying integrity metadata must use CORS. The specification gives the reason in section 5.3: "This specification requires integrity-protected cross-origin requests to use the CORS protocol to ensure that the resource's content is explicitly shared with the requestor." Without that requirement, an attacker could load a resource with a guessed digest and watch for load failures, turning the browser into an oracle for content it is not allowed to read. The specification works the example through with a JSON response containing a username.

By default a resource loaded from a document's HTML is fetched in no-cors mode. MDN states the consequence plainly: "browsers will not allow no-cors requests to use subresource integrity, so a request like this will always fail."

So an integrity attribute without a crossorigin attribute does not give you an unverified script. It gives you no script at all, on every load, for every visitor. A developer who adds half the fix breaks the feature, and the natural repair is to delete the attribute that was doing the work. That asymmetry is why this axis scores whether a pin is in force, separately from whether a pin is present.

There is a second way a pin can be present and inert, and it is quieter. Conformant user agents must support SHA-256, SHA-384 and SHA-512. If you pin using a function the browser does not support, the specification is explicit about what happens: "validation using unsupported hash functions acts like no integrity value was provided." Nothing errors. Nothing reports. The attribute sits in the markup looking like protection.

MDN adds a related subtlety that surprises people who assume a list of hashes means "any of these will do". When several hash functions appear in one attribute, the browser "will first select the set of hashes that were generated using the strongest hash function present" and "will ignore all other hashes". Multiple values of the same function are the alternatives mechanism; multiple functions are an upgrade path, not a fallback.

Where the enforcement mechanism actually lives, as of September 2026

A per-element attribute is an opt-in, and an opt-in that covers most of your scripts covers none of your risk. The interesting recent change is that the enforcement layer has a home.

We checked the obvious place first. The Content Security Policy Level 3 Editor's Draft dated 13 August 2026 contains zero occurrences of the string require-sri-for, the directive most older write-ups name as the way to make integrity mandatory. It mentions Subresource Integrity three times and defines no directive requiring it. We are reporting a measurement of the current draft, not a history of how it got there.

The enforcement mechanism is in the Subresource Integrity specification itself, as a pair of HTTP headers: "The Integrity-Policy and Integrity-Policy-Report-Only HTTP headers enable a document to enforce a policy regarding the integrity metadata requirements on all the subresources it loads of certain destinations." The destinations are script and style. A document that sends

Integrity-Policy: blocked-destinations=(script), endpoints=(integrity-endpoint)

blocks any external script request that arrives without integrity metadata, and any no-CORS external script request, before it is made. MDN describes the intended rollout: use Integrity-Policy-Report-Only first, watch the reporting endpoint, and "once they'd see that no violation reports are being received, they'd know that they can enable blocking using the Integrity-Policy header without risking user-facing breakage."

This is what moves a build from "some tags are pinned" to "an unpinned tag cannot load". It is also what makes the axis measurable from outside: the header is either in the response or it is not.

Vercel

What the framework's own third-party script component says about it

Next.js ships a dedicated component for exactly this job. Its API reference documents loading strategies, worker offloading, event handlers and inline content, and its own examples of third-party loading are bare src attributes pointing at an external host.

We counted. Across the whole next/script API reference page, the string integrity appears zero times, and SRI appears zero times.

That is not a criticism of the framework, which is documenting a loading component rather than a security feature. It is the point of the axis. The path of least resistance for a generated application runs through a component whose documentation never raises the question, which is precisely the condition under which a defect propagates by default rather than by decision.

What this axis measures, and what it does not

This is a measurement of the delivered document, not of anyone's intentions and not of anyone's breach history. It asks what a browser is instructed to execute, from where, and under what verification. It can be run against a built application by a person who did not write it.

It is explicitly not a claim that any builder's output has been attacked, that any CDN is compromised, or that pinning is always the right call. Two of its seven signals exist specifically to stop the rubric rewarding reflexive pinning.

Why this is not already covered

BuilderProof already publishes two axes that sit close to this one. Neither reaches it, and one of them is close enough that the boundary has to be stated in both directions.

Against dependency and supply-chain posture

MDN calls the CDN-compromise case "a supply chain attack", and our dependency and supply-chain posture axis is named for supply chains. They are different links in it.

The boundary: what is installed at build time and recorded in a manifest versus what the browser is told to fetch from somebody else's host when the page loads.

Both directions are real. A project can ship a committed lockfile, a clean software composition analysis run, a CycloneDX bill of materials and a documented patch path, and still deliver an HTML document that executes three unpinned scripts from three hosts it does not own. Conversely a project could pin every external script by hash and still install a hallucinated package at build time.

The strongest form of the argument is that the neighbouring axis's own remedies cannot observe this one's subject. All five of its sub-criteria operate on the package manifest: a committed lockfile, vulnerability scanning of the dependency tree, package provenance against a registry, an SBOM generated for the produced app, and a dependency update path. A script element in the delivered HTML appears in no lockfile, is scanned by no package auditor, is listed in no manifest-derived bill of materials, and is updated by nobody. It is not that the remedy is weak here. It is that the remedy is structurally blind to the event.

Against security headers and response hardening

Our security headers axis scores whether a Content Security Policy is delivered and whether its script-src is meaningful.

The boundary: a policy header declares which origins may execute versus the integrity attribute verifies which bytes arrived.

Both directions again. A strict, nonce-based CSP that allowlists a CDN host permits every byte that host ever serves, including bytes substituted after your audit; the policy is satisfied by the attacker's script because the attacker's script came from the allowlisted origin. Conversely an application could pin every external script by hash and send no CSP at all, leaving injected inline script entirely unconstrained.

One honest complication, because the two posts touch. The security headers post does mention Subresource Integrity, in a different role: as the mechanism behind a hash-based CSP, which lets a build keep static generation instead of paying the dynamic-rendering cost that nonces impose. That is SRI applied to the application's own inline scripts at build time. This axis is SRI applied to somebody else's bytes at request time. Same primitive, opposite direction, different failure.

The relationship with our consent gating axis is a composition, and saying so is more useful than drawing a line that is not there.

The scripts most likely to be third-party hosted, and least likely to be pinnable, are analytics collectors and tag managers. Those are exactly the scripts the consent axis gates. One axis decides whether the script loads; the other decides what arrives if it does. Neither reaches the other's failure. A build can gate a tag manager perfectly behind an answer and still execute whatever that host serves; a build can pin an analytics bundle to a hash and still run it before anyone agreed to anything.

The documented tension, and it is with one of our own axes

Pinning by hash means the bytes cannot change. A security patch to a third-party library is a byte change. So a hash-pinned script is a script the vendor cannot fix for you.

That collides directly with the fifth sub-criterion of our own dependency axis, the dependency update and patch path, and with our stack currency axis. One axis rewards a build for being able to take an upstream fix without ceremony. This one rewards a build for making an upstream change impossible without ceremony. Both are correct about their own subject.

The specification effectively concedes the point, twice, and both concessions are availability mechanisms rather than security ones. Section 3.2.1 allows multiple metadata sets for agility. Section 3.7 says a failed check returns a network error, then adds a note: "Developers wishing to provide a canonical fallback resource (e.g., a resource not served from a CDN, perhaps from a secondary, trusted, but slower source) can catch this error event and provide an appropriate handler to replace the failed resource with a different one."

A specification that ships a fallback pattern alongside its verification mechanism is telling you that verification and availability pull against each other.

We resolve it by classification rather than by choice, which is why signal 6 exists. Third-party scripts split into two classes:

  • Pinnable. A versioned, immutable URL for a library that changes only when you change the URL. There is no reason not to pin these, and a build that does not is simply not taking a free win.
  • Live. A script whose whole purpose is that the vendor updates it without asking, or whose behaviour is configured remotely. Pinning it would break the page on the vendor's next push.

The axis does not require the live class to be pinned. It requires the live class to be named, so that a reader of the build knows which of its third-party code is verified and which is trusted.

The proposed rubric

Seven signals, one hundred points. Each row states the failing case rather than the ideal, because a failing case is something you can check against your own build in an afternoon.

Scroll to see more

SignalWeightWhat a failing case looks like
Externally hosted executable code is pinned22Every script element whose host the project does not control carries no integrity attribute, so the document executes whatever bytes that host returns on the day a visitor arrives, and no test anyone can run locally distinguishes that from the intended case
Pinning is enforced by policy, not per element20Some scripts carry integrity metadata and others do not, with nothing at the document level requiring it, so one forgotten tag, one later edit, or one script injected at runtime reopens the whole surface silently
The pin is in force rather than inert16An integrity attribute is present without a crossorigin attribute, so the request is no-cors and always fails; or the only hash uses a function the user agent does not support, in which case the specification says it behaves as though no integrity value were provided at all
The third-party inventory is enumerated14Nobody involved can state how many distinct hosts the delivered document executes code from, so the question of which are pinned cannot be asked, let alone answered
A pinned resource has a stated update path12Hashes were generated once and no procedure exists for re-pinning when the vendor ships a fix, so the first upstream security patch either breaks the page or is resolved by deleting the attribute
The unpinnable class is named9A script that must stay live, such as a remotely configured tag manager or a payments SDK, is either treated as though it were pinned or not distinguished from the pinnable class at all, so the build's actual trust boundary is undocumented
A failed verification is observable7Nothing listens for the error event and no reporting endpoint is configured, so a failed check presents to the team as a feature that quietly stopped working rather than as a security event

The four postures

The ladder is deliberately uneven. The step from the second rung to the third is much larger than the step from the first to the second, because it is the step from a property of individual tags to a property of the document.

  1. Unpinned. External scripts are identified by URL alone. This is the default, and it is what a generated application ships unless somebody decided otherwise.
  2. Pinned by hand. Some external scripts carry integrity metadata. Nothing requires it, nothing checks it, and coverage is whatever the last editor happened to do.
  3. Policy enforced. An Integrity-Policy header makes integrity metadata mandatory for the script destination, so an unpinned or no-cors external script does not load. Coverage stops depending on discipline.
  4. Enforced and maintained. As above, plus a reporting endpoint that is actually monitored, a written procedure for re-pinning when a vendor publishes a new version, and an explicit list of the resources that cannot be pinned together with the reason.

The ten-step measurement protocol

  1. Build one application from a feature-only prompt. Name no security requirement, no CDN, and no library. The prompt must not contain the word integrity.
  2. Census. From the delivered HTML, list every script element and every link element with rel of stylesheet, preload or modulepreload whose host is not the application's own origin. Record zero as zero; a build with no external executable code is a legitimate and good outcome, not a missing measurement.
  3. Run the control that carries no information, and record that it carries none. Load the page and confirm every one of those requests succeeds. This tells you nothing whatsoever about integrity, because success is what the pinned and unpinned cases both look like. It is in the protocol so that nobody mistakes it for evidence later.
  4. For each external item in the census, record two independent facts: whether an integrity attribute is present, and whether a crossorigin attribute is present. Do not collapse them into one column.
  5. For each integrity value present, fetch the bytes the host is serving now and recompute the digest. openssl dgst -sha384 -binary followed by openssl base64 -A is sufficient. A hash that does not match today is a broken page waiting for a cache to expire.
  6. Substitution probe. Redirect one pinned URL to a local server returning different bytes, and confirm the browser refuses to execute. If it executes, the attribute was decorative and step 4 recorded a false positive.
  7. No-cors probe. Remove the crossorigin attribute from one pinned element and confirm the load now fails rather than merely losing verification. This distinguishes a build that understands the CORS requirement from one that copied a snippet.
  8. Policy probe. Inspect the response headers of the document for Integrity-Policy or Integrity-Policy-Report-Only. Record the difference between them; report-only is a real and sensible rung, not a failure.
  9. Runtime injection probe. After load completes, count the script elements present in the document that were not in the delivered HTML. Every one of them is code that arrived outside whatever coverage steps 4 to 7 measured.
  10. Provenance. Search the build's own generated documentation and configuration for integrity, crossorigin, Integrity-Policy and the names of any CDN hosts found in step 2. Record whether the build says anything at all about the code it fetches from elsewhere.

What this axis deliberately does not measure

Every BuilderProof axis names its own bias, because a scorecard that hides its blind spots is worse than no scorecard.

  1. It is a pre-registration, not a report. No builder is scored here and no placement is published. It enters any composite only after a fixed, published procedure and a review window.
  2. It measures the shape of the delivered document, not observed harm. A build scoring badly has not been attacked, and a build scoring well is not immune. This is a structural measurement.
  3. Its coverage is the specification's coverage, which is narrow. Integrity metadata applies to script elements and to link elements with rel of stylesheet, preload or modulepreload. The specification itself notes that a future revision "is likely to include integrity support for all possible subresources, i.e., a, audio, embed, iframe, img, link, object, script, source, track, and video elements." Today an embedded frame, an image, a worker and a fetch call are all out of scope, and a build cannot be marked down for the specification's boundary.
  4. A hash proves bytes, not intent. A perfectly pinned script can be hostile from its first byte. This axis measures whether the code you chose is the code that runs, not whether choosing it was wise.
  5. Signal 1 could punish the better design, so it is scored to avoid that. A build that self-hosts every asset has no external scripts and therefore nothing to pin. That is a full pass on signal 1, not a blank, and it should never read as a weaker result than a build that loads six CDN scripts and pins five of them. We are stating this explicitly because a rubric that rewarded the presence of integrity attributes would rank the worse architecture higher.
  6. The enforcement header is recent. Integrity-Policy availability varies across engines and a build should not be penalised for declining it on reach grounds. Signal 2 scores whether any document-level mechanism exists, including a build-time check that fails the build on an unpinned external script. It does not mandate one header.
  7. It is a single-run read. A build that injects third-party code only under certain conditions, such as after a consent answer or only in production, may present a cleaner document to this protocol than it presents to a real visitor.

The trap: the same-URL illusion

Every axis BuilderProof proposes names the illusion that hides its defect, because the illusion is usually the reason nobody looked.

Here it is the identifier. A URL is written once, reviewed once, and then treated as though it names a thing. It names an address. The bytes at that address are chosen by somebody else, continuously, for as long as your application is deployed, and you will not be told when they change.

This belongs to a family we keep finding: the defect requires an actor other than the developer to do something, and at test time that actor is cooperating. Every observation available while building is generated by a third party who is currently behaving. The page loads. The library works. The audit you did on Tuesday audited Tuesday's bytes, and the audit is not wrong, it is simply not about Wednesday.

What makes this one distinct from its neighbours in that family is that the evidence does not merely fail to show the defect. The evidence is generated by the party you are trying to protect yourself from. There is no local experiment that distinguishes a pinned page from an unpinned one until the day the distinction matters, and on that day the experiment is run by somebody else.

How to reproduce this reading

  1. Read the Subresource Integrity Editor's Draft in full, not a summary of it. Sections 1.1, 3.2.1, 3.4, 3.7, 3.8, 5.1 and 5.3 carry everything this axis relies on.
  2. Verify the specification's own worked example rather than quoting it. Its published SHA-384 digest for the string alert('Hello, world.'); is H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO, and its SHA-512 digest is Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==. We recomputed both from the source string and both match exactly. Adding a single space before the semicolon changes the digest completely, which is the property the whole mechanism rests on.
  3. Search the Content Security Policy Level 3 draft for require-sri-for yourself, and record what you find rather than what older articles say you will find.
  4. Open your framework's own third-party script documentation and count occurrences of integrity. Report the number, including when it is zero.
  5. Run the ten-step protocol above against one generated application, and publish the census from step 2 even when it is empty.

FAQ

Common questions about the proposed third-party script integrity axis are answered below.

References

Every source below was read in full on 25 September 2026. Quotations are verbatim.

  • Subresource Integrity, W3C Editor's Draft, 20 March 2026. Section 1 introduction, on CDN compromise; section 1.1 goals, on loading a specific script rather than any script at a URL; section 3.1, integrity metadata and the worked SHA-384 example; section 3.2, the mandatory hash functions; section 3.2.1, agility and the behaviour of unsupported hash functions; section 3.4, the note listing the elements a future revision may cover; section 3.7, network error on failure and the fallback-resource note; section 3.8, the Integrity-Policy and Integrity-Policy-Report-Only headers and the blocking algorithm; sections 5.1 to 5.3, non-secure contexts, hash collisions and the CORS requirement. https://w3c.github.io/webappsec-subresource-integrity/
  • MDN, Subresource Integrity. The supply-chain-attack framing; the exact element and rel scope; strongest-function selection and hash ignoring; the CORS and crossorigin requirement and the always-fails consequence of no-cors; integrity policy and the report-only deployment path. https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
  • Content Security Policy Level 3, W3C Editor's Draft, 13 August 2026. Read for the presence or absence of a directive requiring integrity metadata; require-sri-for occurs zero times. https://w3c.github.io/webappsec-csp/
  • Next.js, Script component API reference. Loading strategies, worker offloading and inline content; read for occurrences of integrity, of which there are none. https://nextjs.org/docs/app/api-reference/components/script

Cite this benchmark

Plain text
BuilderProof editorial team. "Who Can Change the Script After You Ship It? A Proposed Axis for Third-Party Script Integrity (September 2026)". BuilderProof, September 2026. https://www.builderproof.org/benchmarks/who-can-change-the-script-after-you-ship-it-script-integrity-axis-september-2026.
BibTeX
@misc{builderproof-who-can-change-the-script-after-you-ship-it-script-integrity-axis-september-2026,
  title  = {{Who Can Change the Script After You Ship It? A Proposed Axis for Third-Party Script Integrity (September 2026)}},
  author = {{BuilderProof editorial team}},
  year   = {2026},
  month  = {sep},
  howpublished = {\url{https://www.builderproof.org/benchmarks/who-can-change-the-script-after-you-ship-it-script-integrity-axis-september-2026}},
  note   = {BuilderProof, builderproof.org}
}

Frequently asked questions

Is adding an integrity attribute to every external script the recommendation here?

No, and two of the seven signals exist to stop the rubric implying it. Scripts split into a pinnable class, where a versioned immutable URL changes only when you change it, and a live class, where the vendor updates the file on purpose and pinning it would break the page on their next push. The axis requires the live class to be named rather than pinned, so that a reader of the build can tell which third-party code is verified and which is trusted. It also scores a build that self-hosts everything, and therefore has nothing external to pin, as a full pass rather than a blank.

Does an integrity attribute without a crossorigin attribute just mean the script is unverified?

No. It means the script does not load at all, on every request. Cross-origin requests carrying integrity metadata must use CORS, and MDN states that browsers will not allow no-cors requests to use subresource integrity, so such a request will always fail. The failure direction surprises people: the half-fix breaks the feature, and the natural repair is to delete the attribute that was doing the work. That is why the rubric scores whether a pin is in force separately from whether a pin is present.

Is this the same thing as the dependency and supply-chain axis?

No, although both concern supply chains and MDN uses that exact phrase for the case this axis covers. The boundary is what is installed at build time and recorded in a manifest against what the browser is told to fetch from somebody else's host when the page loads. All five sub-criteria of the dependency axis operate on the package manifest, so a script element in the delivered HTML appears in no lockfile, is scanned by no package auditor and is listed in no manifest-derived bill of materials. A project can pass that axis completely and still execute unpinned code from three hosts it does not own.

Does a Content Security Policy already cover this?

No. A policy header declares which origins may execute; the integrity attribute verifies which bytes arrived. A strict policy that allowlists a content delivery network permits every byte that host ever serves, including bytes substituted after an audit, because the substituted script still comes from the allowlisted origin. The two are complementary rather than overlapping, and a build can do either one well while doing nothing about the other.

Why does the axis care about a header rather than just counting pinned scripts?

Because a per-element attribute is an opt-in, and an opt-in that covers most of your scripts covers none of your risk. One forgotten tag or one script injected at runtime reopens the surface, and nothing in the document notices. The Integrity-Policy and Integrity-Policy-Report-Only headers move the question from a property of individual tags to a property of the document, and a report-only deployment is counted as a real rung rather than a failure.