fix(plugins): inject globalThis.require for CJS interop in jiti-loaded extensions#13109
fix(plugins): inject globalThis.require for CJS interop in jiti-loaded extensions#13109mcaxtr wants to merge 5 commits intoopenclaw:mainfrom
Conversation
bd8d05a to
55a12d2
Compare
55a12d2 to
dc8a9e6
Compare
dc8a9e6 to
0a3f783
Compare
0a7cbca to
1edfc49
Compare
bfc1ccb to
f92900f
Compare
04091d6 to
78046a8
Compare
0558af0 to
deb65d0
Compare
13dce52 to
fbc0d97
Compare
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟠 Uncontrolled module resolution root via createRequire(candidate.source) when plugin path contains symlinks
Description
Because Node computes Security impact:
Vulnerable code: // Anchor require() resolution to the plugin's directory so its CJS
// dependencies resolve packages from the plugin's own node_modules.
globalThis.require = createRequire(candidate.source);RecommendationAnchor Suggested fix: // Use the canonical path returned by openBoundaryFileSync()
globalThis.require = createRequire(safeSource);Optionally, also consider:
2. 🔵 Global
|
| Property | Value |
|---|---|
| Severity | Low |
| CWE | CWE-703 |
| Location | src/plugins/loader.ts:712-716 |
Description
loadOpenClawPlugins() mutates globalThis.require to a per-plugin createRequire() so jiti-loaded code can call require(). However, restoration back to the original savedRequire happens only at the end of the function and is not protected by try/finally.
This means any uncaught exception between setting savedRequire and the final restore (including attacker-controlled exceptions from plugin exports) can leave the process-wide globalThis.require permanently pointing at a plugin directory.
Impact:
- Denial of service / reliability break: subsequent code (or later plugin loads) may resolve modules from the wrong location or crash unexpectedly.
- Cross-plugin contamination: a plugin can intentionally trigger an exception after
globalThis.requireis switched, causing other code to run under altered module-resolution semantics.
Vulnerable pattern:
const savedRequire = globalThis.require;
...
globalThis.require = createRequire(candidate.source);
...
// not in finally
globalThis.require = savedRequire;Because resolvePluginModuleExport(mod), config/schema validation, or other logic after import can throw outside the existing try/catch blocks, a malicious plugin can intentionally prevent restoration and leave global state corrupted.
Recommendation
Use try/finally to guarantee restoration even if any plugin-related step throws. Ideally, restore per plugin as well to minimize the window of global mutation.
Example (outer guard + per-plugin guard):
const savedRequire = globalThis.require;
try {
for (const candidate of discovery.candidates) {
const pluginRequire = createRequire(candidate.source);
const prior = globalThis.require;
globalThis.require = pluginRequire;
try {
const mod = getJiti()(safeSource) as OpenClawPluginModule;
// ... resolve exports, validate config, register, etc.
} finally {
globalThis.require = prior;
}
}
} finally {
globalThis.require = savedRequire;
}Also consider avoiding process-global state entirely by passing a scoped require into plugin evaluation if the loader/runtime supports it.
Analyzed PR: #13109 at commit e894379
Last updated on: 2026-03-05T04:22:20Z
fbc0d97 to
5008a05
Compare
5008a05 to
e894379
Compare
Summary
Fixes #12854
Extensions loaded via jiti run in an ESM context where
requireis unavailable. CJS dependencies that internally callrequire()(e.g.@vector-im/matrix-bot-sdkcallingrequire("events")) crash at runtime withReferenceError: require is not defined.This PR:
globalThis.requireshim at module load time viacreateRequire(import.meta.url)so CJS packages always have a usablerequireglobalThis.requireto each plugin's source path beforejiti()evaluates it, so CJS dependencies resolve from the plugin's ownnode_modulesglobalThis.requirearound the plugin loading loop so runtime code outside the loader is unaffectedTest plan
injects globalThis.require for CJS interop in jiti-loaded plugins— verifies the shim exists and resolves built-in modulesloads plugins whose CJS dependencies call require() at runtime— creates a real CJS helper that callsrequire("node:path"), loads a plugin that depends on it, verifiesstatus === "loaded"pnpm build && pnpm checkcleanGreptile Overview
Greptile Summary
This change adjusts the plugin loader so extensions evaluated via
jiti(ESM context) have arequireavailable for CommonJS interop. It injects aglobalThis.requireshim viacreateRequire(import.meta.url), re-anchorsglobalThis.requireto each plugin’s source path beforejiti()evaluation so CJS dependencies resolve from the plugin’s ownnode_modules, and saves/restores any pre-existingglobalThis.requirearound the plugin loading loop. Tests are added to verify the shim exists and can resolve built-in modules, and that plugins with CJS dependencies callingrequire()at runtime load successfully.Confidence Score: 5/5