No global state pollution
Why it matters
When plugins receive mutable references to shared objects — config, process.env, shared caches — any plugin can modify state that all other plugins and the host depend on. Plugin A changes config.apiUrl and Plugin B silently uses the wrong endpoint. A plugin calling require('dotenv').config() overwrites environment variables for every module in the process. CWE-454 (Unintended Variable Set from External Scope) and CWE-829 cover this class. The failure mode is load-order-dependent and nearly impossible to reproduce in isolation, making it one of the hardest bugs to diagnose in plugin ecosystems.
Severity rationale
High because plugins writing to shared global state or mutating passed-by-reference objects create load-order-dependent bugs between plugins that are invisible during individual plugin testing.
Remediation
Freeze or deep-clone any object before passing it to plugin code. For config objects, a Proxy that throws on write gives a clear error attributing the mutation attempt to the offending plugin.
// Freeze config before passing to plugins:
const pluginConfig = Object.freeze(structuredClone(hostConfig));
// Or use a Proxy to prevent writes:
const readonlyConfig = new Proxy(config, {
set() { throw new Error('Config is read-only for plugins'); },
deleteProperty() { throw new Error('Config is read-only for plugins'); }
});
Detection
-
ID:
no-global-pollution -
Severity:
high -
What to look for: Check whether the plugin system prevents plugins from modifying global state, monkey-patching host code, or polluting shared namespaces. Look for:
- Plugins modifying
global,window,globalThis, orprocess.env - Plugins monkey-patching prototype methods (e.g.,
Array.prototype.customMethod = ...) - Plugins modifying shared objects passed by reference (e.g., mutating the host config object)
- Module-level side effects that affect other plugins (e.g.,
require('dotenv').config()modifyingprocess.envfor all modules) - Whether the host passes frozen/readonly objects or mutable references to plugins Prevention patterns:
- Fastify:
Object.freeze()on decorated properties, encapsulated plugin context - Node.js worker threads: separate V8 isolates, no shared global scope
- Figma: iframe-based sandbox with
postMessagecommunication — complete isolation
- Plugins modifying
-
Pass criteria: Count all plugin files and check for global variable assignments. The plugin system prevents or discourages global state pollution. At least one of: plugins run in isolated scopes (workers, iframes, separate processes), shared objects are frozen or readonly, the plugin API does not expose mutable global references, OR documentation explicitly prohibits global modification with enforcement in reviews/testing. 0% of plugin code may write to global scope or modify host prototypes.
-
Fail criteria: Quote the specific global assignments or prototype modifications found. Do not pass when any plugin modifies
window,global, or built-in prototypes. Plugins have unrestricted access to global state. No frozen objects, no isolation, no documentation warning against global modification. Plugins can freely modifyglobal,process.env, shared config objects, or prototype methods, affecting all other plugins and the host. -
Skip (N/A) when: Plugins run in a truly isolated environment by design (separate processes or VMs with no shared memory) where global pollution is architecturally impossible.
-
Detail on fail:
"Plugins receive the config object by mutable reference. Plugin A can modify config.apiUrl and Plugin B will see the changed value. No Object.freeze(), no defensive copies, no isolation. Plugins also have access to process.env and can modify environment variables visible to the host and all other plugins." -
Remediation: Global state pollution is the most common source of plugin-to-plugin conflicts. Two plugins that both modify the same global create an intermittent bug that depends on load order and is nearly impossible to reproduce.
// Freeze config before passing to plugins: const pluginConfig = Object.freeze(structuredClone(hostConfig)); // Or use a Proxy to prevent writes: const readonlyConfig = new Proxy(config, { set() { throw new Error('Config is read-only for plugins'); }, deleteProperty() { throw new Error('Config is read-only for plugins'); } });
External references
- cwe · CWE-829 — Inclusion of Functionality from Untrusted Control Sphere — plugin code must not modify shared global state or host prototypes
- cwe · CWE-454 — External Initialization of Trusted Variables or Data Stores — plugins writing to process.env or global can compromise host integrity
- owasp:2021 · A03 — Injection — prototype pollution is a JavaScript injection vector
Taxons
History
- 2026-04-18·v1.0.0·Initial import from plugin-extension-architecture·automated