When removing a plugin leaves its intervals ticking, its event listeners attached, and its registered routes still responding to requests, the application enters a state where disabled plugins continue to affect users. CWE-404 (Improper Resource Shutdown) is the direct classification. In practice this means: a removed analytics plugin continues to log user data; a deactivated payment plugin still intercepts checkout webhooks; orphaned timers accumulate until they cause performance degradation. The only reliable remediation — a full restart — creates downtime every time an operator needs to remove a plugin.
Medium because absent resource cleanup on plugin removal leaves intervals, listeners, and routes active after disable, requiring a full application restart to fully remove a plugin's effects.
Track every resource a plugin registers in a per-plugin disposables list. On removal, call the plugin's destroy() method and then dispose every tracked resource, so cleanup happens even when plugin authors forget to do it themselves.
class PluginManager {
private pluginResources = new Map<string, Disposable[]>();
registerHook(pluginName: string, hook: string, handler: Function) {
const disposable = this.hooks.on(hook, handler);
this.pluginResources.get(pluginName)?.push(disposable);
}
async removePlugin(pluginName: string) {
await this.plugins.get(pluginName)?.destroy();
for (const disposable of this.pluginResources.get(pluginName) ?? []) {
disposable.dispose();
}
this.pluginResources.delete(pluginName);
}
}
ID: plugin-extension-architecture.isolation-security.resource-cleanup
Severity: medium
What to look for: Check what happens when a plugin is disabled, removed, or the host shuts down. Look for:
deactivate/destroy lifecycle hook is calledsetInterval or setTimeout not clearedPass criteria: Count all resource types allocated by plugins. The host performs cleanup when a plugin is removed: calls the plugin's cleanup lifecycle method, removes all hook handlers registered by that plugin, and releases resources allocated on the plugin's behalf. At least one of these cleanup paths is implemented. 100% of allocated resources must have cleanup logic on plugin unload.
Fail criteria: No cleanup on plugin removal. Disabling a plugin leaves its handlers running, its intervals ticking, its connections open. The only way to fully remove a plugin's effects is to restart the host.
Skip (N/A) when: Plugins cannot be removed or disabled at runtime — they are only configured at startup and the process exits when done (e.g., a build tool plugin that runs once and exits).
Detail on fail: "When a plugin is removed via the admin UI, its entry is deleted from the plugins config, but no cleanup occurs. The plugin's setInterval handlers continue running, its registered routes still respond to requests, and its event listeners still fire. Only a full server restart removes the plugin's effects."
Remediation: Automatic cleanup is a safety net for plugin authors who forget to clean up in their destroy() method. The host should track what each plugin registers and remove it all on deactivation.
class PluginManager {
private pluginResources = new Map<string, Disposable[]>();
registerHook(pluginName: string, hook: string, handler: Function) {
const disposable = this.hooks.on(hook, handler);
this.pluginResources.get(pluginName)?.push(disposable);
}
async removePlugin(pluginName: string) {
// Call plugin's own cleanup:
await this.plugins.get(pluginName)?.destroy();
// Host cleanup — remove everything the plugin registered:
for (const disposable of this.pluginResources.get(pluginName) ?? []) {
disposable.dispose();
}
this.pluginResources.delete(pluginName);
}
}