Web functionality from an untrusted source

Web functionality loaded from an untrusted source

Description

Loading scripts, styles, or modules from an untrusted domain exposes the application to supply-chain risk. A loaded script runs in the page's context. A domain ownership change, server compromise, or DNS tampering may let an attacker deliver code that steals accessible cookies or tokens, changes the DOM, records keystrokes, or directs users to phishing pages. Malicious scripts were distributed through polyfill.io in 2024.

Potential impact

  • Malicious code running with the page's permissions
  • Theft of accessible cookies, local storage data, or OAuth tokens
  • Leakage of form entries, payment details, or personal information
  • DOM changes that present fake login or payment forms
  • Compromise spreading to pages and users that load the affected script

Remediation

  • Remove resources from untrusted domains such as polyfill.io.
  • Prefer self-hosted, verified, fixed versions of required polyfills and libraries.
  • If a CDN is necessary, choose a trusted provider, pin the version, and use SRI (integrity and crossorigin).
  • For dynamic loading through import(), createElement, or fetch followed by eval, keep allowed domains fixed in code or build configuration and restrict runtime changes.
  • Limit CSP's script-src to 'self' and a strict allow-list; use 'strict-dynamic', nonces, or 'sha256-...' hashes where appropriate.
  • Reassess legacy browser requirements and load only necessary polyfills based on feature detection.
  • Monitor resource changes and integrity, and review third-party dependencies regularly.

Examples

Before

javascript
// server.js (Node/Express) - before
import express from 'express';
const app = express();

// Configurable through an environment variable, with polyfill.io as the default.
// A compromised supplier can deliver malicious scripts to users.
app.get('/', (req, res) => {
  const polyfillUrl = process.env.POLYFILL_URL || 'https://polyfill.io/v3/polyfill.min.js?features=default';
  res.send(`<!doctype html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>App</title>
      <script src="${polyfillUrl}"></script>
    </head>
    <body>
      <h1>Hello</h1>
    </body>
  </html>`);
});

app.listen(3000);

After

javascript
// server.js (Node/Express) - after
import express from 'express';
const app = express();

// 1) Self-host: deploy a verified fixed-version file to /assets during the build.
app.use('/assets', express.static('public/assets', { immutable: true, maxAge: '1y' }));

app.get('/', (req, res) => {
  // Serve a verified file at a fixed path without relying on an external domain.
  // For an external CDN: pin the exact version and set integrity + crossorigin.
  res.setHeader('Content-Security-Policy', "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'");
  res.send(`<!doctype html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>App</title>
      <script src="/assets/polyfills-4.8.0.min.js" defer></script>
    </head>
    <body>
      <h1>Hello</h1>
    </body>
  </html>`);
});

app.listen(3000);

Explanation:

  • Before: The page loads a script directly from polyfill.io. A compromised supplier may execute malicious JavaScript in the page's context. A runtime-configurable URL also requires control over who can change it.
  • After: A verified file is self-hosted at a fixed path, and CSP permits same-origin scripts. For external CDNs, pin versions and use SRI to check against a trusted original, alongside fixed allowed domains. These measures reduce supply-chain risk.

References