SharePointVault ~/gotchas/spfx ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdM↓ spfx-sri-integrity-via-manifest-path.md
sharepoint-vault›gotchas›spfx›spfx-sri-integrity-via-manifest-path.mdhistoryedit on GitHub
● GOTCHAspfx· 7 min read

SPFx adds Subresource Integrity to a script when the manifest path is { path, integrity }

applies to: SPFx 1.2x web parts and extensions that load code at runtime from your own CDN with SPComponentLoader.loadComponent (measured on SPFx 1.22.2, SharePoint Online, November 2026)reviewed: 2026-11-11#spfx#security#sri#cdn#sp-loader#app-catalog#supply-chain#list-view-command-set

If your web part fetches a component manifest from a CDN and calls SPComponentLoader.loadComponent(manifest), whoever can write to that CDN runs code in every tenant. You can pin the exact bytes: in loaderConfig.scriptResources, give each type: "path" resource path: { "path": "bundle_abc.js", "integrity": "sha384-…" }. SharePoint then renders <script integrity="sha384-…" crossorigin="anonymous"> and the browser refuses a bundle that does not match (Script error). Verify the manifest itself against a hash you trust before calling loadComponent. Keep that trust anchor out of the CDN by pinning the bootstrap the same way from the web part manifest in the .sppkg. Do not put the bootstrap file itself into the package: ClientSideAssets in the App Catalog is not readable by guests. When you test this in the console, use a unique manifest version and a unique bundle path per attempt, otherwise SPFx returns the module it already loaded and the test lies.

Když web part načítá manifest komponenty z CDN přes SPComponentLoader.loadComponent, dej každému zdroji typu path tvar { path, integrity: "sha384-…" }. SharePoint vloží <script integrity crossorigin="anonymous"> a prohlížeč podvržený bundle nespustí. Manifest ověř proti otisku, kterému věříš. Kotvu důvěry drž mimo CDN: otiskem zavaděče v manifestu .sppkg, zavaděč sám zůstane na CDN pod neměnným jménem. ClientSideAssets v App Catalogu host nepřečte. V konzoli testuj s jiným version a jinou cestou bundlu pro každý pokus, jinak SPFx vrátí už načtený modul.

Symptom

You built a "thin" SPFx package: the web part in the .sppkg only reads which version to run, downloads <version>/manifest.json from your CDN and calls SPComponentLoader.loadComponent. Nothing checks what it downloads. A stolen CDN deploy token, a hijacked DNS record or a compromised CI job is enough to run arbitrary code with the permissions of your app in every customer tenant.

While adding a check you also see confusing test results:

  • a manifest with a deliberately wrong integrity still reports "loaded" in the console;
  • a second loadComponent on the same page fails even though the manifest is now correct;
  • the SHA-384 you computed from the file on disk does not match what the CDN serves.

Cause

  1. SRI support is in sp-loader, but only for the object form of path. When a script resource's path is an object, sp-loader resolves it to { path, integrity } and SharePoint adds the integrity and crossorigin="anonymous" attributes to the script tag. A plain string path gets no integrity. On the tenant where this was measured the behaviour sits behind the SharePoint flight SpfxSubresourceIntegrity (60737), which was on. Check a customer tenant with document.querySelectorAll('script[integrity]') on a page that runs your component.
  2. SPFx caches components by id + version. If every runtime build shares the manifest version (common when the version lives elsewhere), a second "version" on the same page resolves to the already-loaded module. The SRI check never runs.
  3. requirejs caches by script path. A path that failed once stays failed on that page.
  4. CRLF in the working copy. A GitHub Pages repository with core.autocrlf=true and no .gitattributes can have CRLF on disk while Pages serves the LF bytes stored in git. A hash computed from disk does not match the served file.

Fix

Inject integrity before loading (the hashes come from a source you trust, not from the CDN response itself):

// manifest bytes were already checked against a trusted sha384 of the manifest file
for (const name of Object.keys(m.loaderConfig.scriptResources)) {
  const r = m.loaderConfig.scriptResources[name];
  if (r.type === 'component') continue;            // host-provided (react, sp-*): leave alone
  if (r.type !== 'path') throw new Error('unsupported resource ' + name);
  const p = typeof r.path === 'string' ? r.path : r.path.path;
  if (/^[a-z]+:|^\/\/|\.\./i.test(p)) throw new Error('bad path ' + name);
  r.path = { path: p, integrity: trustedSri[name] }; // 'sha384-…'
}
// every internalModuleBaseUrls entry must start with your own CDN folder
await SPComponentLoader.loadComponent(m);

Where to keep the trust anchor. A public key or hash list inside a bootstrap served from the same CDN is replaced together with the bootstrap. Pin the bootstrap itself from the .sppkg:

  • After the build, copy the web part bundle to an immutable, content-addressed name on the CDN, for example my-app-loader_<contenthash>.js.
  • Rewrite the web part manifest in release/manifests (it goes into the .sppkg) to path: { path: "my-app-loader_<contenthash>.js", integrity: "sha384-…" }. package-solution accepts the object form.
  • The page now loads the bootstrap from the CDN with integrity taken from the App Catalog. A replaced bootstrap on the CDN does not run.
  • Never overwrite or prune that file while any App Catalog may point at it.
  • Sign the list of versions and hashes the bootstrap trusts (for example ECDSA P-256 with WebCrypto) and keep only the public key in the bootstrap.

Do not move the bootstrap into the .sppkg if guests use the app. "includeClientSideAssets": true works technically. In SPFx 1.22 (spfx-heft-plugins), heft package-solution --production then:

  • packs every file in release/assets into ClientSideAssets/;
  • rewrites internalModuleBaseUrls of every manifest in release/manifests to HTTPS://SPCLIENTSIDEASSETLIBRARY/.

SharePoint then serves the files from /sites/appcatalog/ClientSideAssets/<solution id>/. On the tenant where this was measured, that library had exactly two read grants:

  • Everyone except external users;
  • the read-only tenant admin role.

External (guest) users therefore cannot load the bootstrap, unless the tenant serves client-side assets through the Office 365 public CDN. A test under an admin account will not show it. Check roleassignments of the target library before you move code there.

Hash what the CDN serves: take the bytes from the git index (git cat-file blob :<path>), not from the working copy.

Testing in the console without lying to yourself:

  • give each attempt its own manifest version and its own bundle path;
  • reload the page after a failed attempt;
  • window._spComponentLoader is available on modern pages.

Testing the real bootstrap without touching the CDN: if the bootstrap remembers the highest signed sequence number it has seen in localStorage (rollback protection), set that key to a huge number and reload. The bootstrap must refuse to start the app. Restore the key afterwards.

Measured

SharePoint Online, SPFx 1.22.2, November 2026, our own tenant:

Manifest given to loadComponent Result
path: "bundle.js" (string) script tag without integrity
path: { path, integrity: <correct sha384> } tag with integrity + crossorigin="anonymous", module loads
path: { path, integrity: <wrong sha384> }, unique version and path Script error, module not loaded
wrong integrity, same id + version as an already loaded module "loads" – the cached module is returned

Same tenant, same day, by page type:

Page Script integrity on the tag
Modern site page (SitePages/*.aspx) web part entry bundle from a manifest with { path, integrity } yes
Modern site page library loaded with loadComponent and { path, integrity } yes
List or library view (Lists/*/AllItems.aspx, Forms/AllItems.aspx) tenant-wide ListView Command Set entry bundle (the manifest with sha384 is in the page HTML) no
List or library view library loaded by that command set with loadComponent and { path, integrity } no
List or library view tenant-wide Application Customizer entry bundle (manifest with sha384) no

Both page types loaded the scripts through RequireJS (data-requiremodule on the tag). Lazy chunks of the library did get integrity on the list page too, because the library's own webpack runtime sets it, not SharePoint.

Gotchas

  • List and library pages ignore manifest integrity (measured November 2026). A ListView Command Set there runs unverified by the browser even with a correct { path, integrity } manifest, and so does any library it loads with loadComponent. A tenant-wide Application Customizer on a library view was measured too (November 2026): its entry bundle had no integrity either. Other extension types (Field Customizer, Form Customizer) were not measured — check them the same way before you rely on SRI. Measure on the page where the component actually runs: [...document.scripts].filter(s => s.src.includes('<bundle>')).map(s => s.hasAttribute('integrity')). Use hasAttribute: the integrity property is an empty string when the attribute is missing. For the libraries your extension loads, you can check the bytes yourself first — await fetch(url, { integrity, mode: 'cors' }) rejects on a mismatch — and refuse to call loadComponent when it fails. The extension's own entry bundle stays out of reach, because SharePoint loads it.

  • Lazy chunks are not covered. The webpack runtime inside the verified entry bundle creates their script tags. Embed a chunk-hash map in the entry bundle (it is verified with it) and set integrity in the runtime, or avoid lazy chunks in code you load this way.

  • Without the flight there is no integrity attribute. You are left with checking the manifest hash, and there is a window between that check and the browser fetching the bundle.

  • A bundle already on the page is reused. If an older bootstrap on the same page loaded the same bundle path without integrity, SharePoint will not check it again.

  • Changing the bootstrap means a new .sppkg. Keep it minimal and stable, and put everything else in the library on the CDN.

  • Scripts your own code loads are not covered either. Vendor libraries injected with <script>, worker code fetched into a blob: URL and importScripts inside a worker bypass both the manifest and the chunk map – see spfx-sri-scripts-your-bundle-loads-itself.md.

Was this helpful?ask or add your experience on GitHub →
Saved you an hour? A star on GitHub helps other SharePoint people find the vault.★ Star on GitHub
last reviewed: 2026-11-11last updated: 2026-11-11historyedit on GitHubview source