From 2f5c0155dcfb27d19bff0fddd27733fa43a5442a Mon Sep 17 00:00:00 2001
From: Beqa <91750923+BeqaKs@users.noreply.github.com>
Date: Wed, 4 Nov 2026 15:41:11 +0400
Subject: [PATCH] fix(web): omit importer integrity for the trusted resolver
stub (#8693)
* fix(web): omit importer integrity for the trusted resolver stub
Import-map integrity that covers the selected importer was applied to REA's
replacement resolver script. Strip only those integrity entries from the
installed map so native resolution can proceed while preserving dependency
integrity and the caller-selected map identity.
Fixes #8544
* fix(web): keep __proto__ import-map integrity keys on a null prototype
---
src/browser/modules/NativeModuleResolver.ts | 3 +-
.../importMapForTrustedResolver.test.ts | 98 +++++++++++++++++++
.../modules/importMapForTrustedResolver.ts | 75 ++++++++++++++
3 files changed, 175 insertions(+), 1 deletion(-)
create mode 100644 src/browser/modules/importMapForTrustedResolver.test.ts
create mode 100644 src/browser/modules/importMapForTrustedResolver.ts
diff --git a/src/browser/modules/NativeModuleResolver.ts b/src/browser/modules/NativeModuleResolver.ts
index 4383a27e0c..3cc45301e4 100644
--- a/src/browser/modules/NativeModuleResolver.ts
+++ b/src/browser/modules/NativeModuleResolver.ts
@@ -33,6 +33,7 @@ import type {
} from "../../domain/webModuleTrace.js";
import { webModuleResolutionSchema } from "../../domain/webModuleTrace.js";
import { ProviderStartupDeadline } from "../../process/ProviderDeadline.js";
+import { importMapForTrustedResolver } from "./importMapForTrustedResolver.js";
const OPERATION = "trace_web_module_imports";
const TIMEOUT_MS = 20000;
@@ -367,7 +368,7 @@ const resolveInBrowser = async (
await route.fulfill({
status: 200,
contentType: "text/html",
- body: `${input.importMap === null ? "" : ``}`,
+ body: `${input.importMap === null ? "" : ``}`,
});
} else if (
!stubSent &&
diff --git a/src/browser/modules/importMapForTrustedResolver.test.ts b/src/browser/modules/importMapForTrustedResolver.test.ts
new file mode 100644
index 0000000000..c1ae0f6200
--- /dev/null
+++ b/src/browser/modules/importMapForTrustedResolver.test.ts
@@ -0,0 +1,98 @@
+import { describe, expect, it } from "vitest";
+import { importMapForTrustedResolver } from "./importMapForTrustedResolver.js";
+
+describe("importMapForTrustedResolver", () => {
+ const baseUrl = "https://app.test/index.html";
+ const importerUrl = "https://app.test/main.js";
+
+ it("omits integrity that covers the selected importer URL", () => {
+ const map = {
+ imports: { dep: "/dep.js" },
+ integrity: {
+ "https://app.test/main.js": "sha256-importer",
+ "https://app.test/dep.js": "sha256-dep",
+ },
+ };
+ expect(importMapForTrustedResolver(map, importerUrl, baseUrl)).toEqual({
+ imports: { dep: "/dep.js" },
+ integrity: { "https://app.test/dep.js": "sha256-dep" },
+ });
+ });
+
+ it("resolves relative integrity keys against the import-map base URL", () => {
+ const map = {
+ imports: { dep: "./dep.js" },
+ integrity: {
+ "./main.js": "sha256-importer",
+ "./dep.js": "sha256-dep",
+ },
+ };
+ expect(
+ importMapForTrustedResolver(map, "https://app.test/main.js", baseUrl),
+ ).toEqual({
+ imports: { dep: "./dep.js" },
+ integrity: { "./dep.js": "sha256-dep" },
+ });
+ });
+
+ it("matches importer integrity across query and fragment differences", () => {
+ const map = {
+ imports: { dep: "/dep.js" },
+ integrity: {
+ "https://app.test/main.js": "sha256-importer",
+ "https://app.test/dep.js": "sha256-dep",
+ },
+ };
+ expect(
+ importMapForTrustedResolver(
+ map,
+ "https://app.test/main.js?build=1#entry",
+ baseUrl,
+ ),
+ ).toEqual({
+ imports: { dep: "/dep.js" },
+ integrity: { "https://app.test/dep.js": "sha256-dep" },
+ });
+ });
+
+ it("drops an empty integrity member after removing the importer entry", () => {
+ const map = {
+ imports: { dep: "/dep.js" },
+ integrity: { "https://app.test/main.js": "sha256-importer" },
+ };
+ expect(importMapForTrustedResolver(map, importerUrl, baseUrl)).toEqual({
+ imports: { dep: "/dep.js" },
+ });
+ });
+
+ it("preserves maps without integrity and dependency-only integrity", () => {
+ const without = { imports: { dep: "/dep.js" } };
+ expect(importMapForTrustedResolver(without, importerUrl, baseUrl)).toBe(
+ without,
+ );
+ const dependencyOnly = {
+ imports: { dep: "/dep.js" },
+ integrity: { "https://app.test/dep.js": "sha256-dep" },
+ };
+ expect(
+ importMapForTrustedResolver(dependencyOnly, importerUrl, baseUrl),
+ ).toBe(dependencyOnly);
+ });
+
+ it("preserves a dependency integrity key named __proto__ without prototype pollution", () => {
+ const integrity = Object.assign(Object.create(null), {
+ "https://app.test/main.js": "sha256-importer",
+ "https://app.test/dep.js": "sha256-dep",
+ });
+ integrity["__proto__"] = "sha256-proto-dep";
+ const map = { imports: { dep: "/dep.js" }, integrity };
+ const next = importMapForTrustedResolver(map, importerUrl, baseUrl);
+ const nextIntegrity = next.integrity as { readonly [key: string]: string };
+ expect(nextIntegrity["https://app.test/dep.js"]).toBe("sha256-dep");
+ expect(
+ Object.getOwnPropertyDescriptor(nextIntegrity, "__proto__")?.value,
+ ).toBe("sha256-proto-dep");
+ expect(Object.getPrototypeOf(nextIntegrity)).toBe(null);
+ expect(Object.prototype.hasOwnProperty.call({}, "__proto__")).toBe(false);
+ });
+});
diff --git a/src/browser/modules/importMapForTrustedResolver.ts b/src/browser/modules/importMapForTrustedResolver.ts
new file mode 100644
index 0000000000..f4e254e2e8
--- /dev/null
+++ b/src/browser/modules/importMapForTrustedResolver.ts
@@ -0,0 +1,75 @@
+import type { JsonValue } from "../../domain/jsonValue.js";
+
+/**
+ * Build the import map installed for REA's trusted resolver stub.
+ *
+ * Chromium applies import-map `integrity` to the module script URL. The
+ * resolver fulfills that URL with REA's replacement bytes, so integrity that
+ * covers the selected importer must not be installed. Dependency integrity and
+ * the caller-selected map identity remain unchanged in Evidence.
+ */
+export const importMapForTrustedResolver = (
+ map: { readonly [key: string]: JsonValue },
+ importerUrl: string,
+ baseUrl: string,
+): { readonly [key: string]: JsonValue } => {
+ const integrity = map.integrity;
+ if (
+ integrity === null ||
+ typeof integrity !== "object" ||
+ Array.isArray(integrity)
+ )
+ return map;
+ const importer = importerModuleIdentity(importerUrl, baseUrl);
+ const nextIntegrity: { [key: string]: JsonValue } = Object.create(null);
+ let removed = false;
+ for (const [key, value] of Object.entries(integrity)) {
+ if (integrityKeyMatchesImporter(key, baseUrl, importer)) {
+ removed = true;
+ continue;
+ }
+ nextIntegrity[key] = value;
+ }
+ if (!removed) return map;
+ if (Object.keys(nextIntegrity).length === 0) {
+ const { integrity: _omit, ...rest } = map;
+ return rest;
+ }
+ return { ...map, integrity: nextIntegrity };
+};
+
+const importerModuleIdentity = (
+ importerUrl: string,
+ baseUrl: string,
+): { readonly hrefs: ReadonlySet; readonly moduleKey: string } => {
+ const absolute = new URL(importerUrl, baseUrl);
+ const withoutHash = new URL(absolute.href);
+ withoutHash.hash = "";
+ return {
+ hrefs: new Set([absolute.href, withoutHash.href]),
+ // Chromium's integrity lookup reports the module URL without search/hash.
+ moduleKey: `${absolute.origin}${absolute.pathname}`,
+ };
+};
+
+const integrityKeyMatchesImporter = (
+ key: string,
+ baseUrl: string,
+ importer: {
+ readonly hrefs: ReadonlySet;
+ readonly moduleKey: string;
+ },
+): boolean => {
+ try {
+ const resolved = new URL(key, baseUrl);
+ const withoutHash = new URL(resolved.href);
+ withoutHash.hash = "";
+ return (
+ importer.hrefs.has(resolved.href) ||
+ importer.hrefs.has(withoutHash.href) ||
+ `${resolved.origin}${resolved.pathname}` === importer.moduleKey
+ );
+ } catch {
+ return false;
+ }
+};