Do You Really Need Plugins? Designing Secure, Scalable Plugin Systems
This article explores when plugins are truly necessary, outlines design principles and SOLID guidelines for building robust plugin architectures, demonstrates a step‑by‑step implementation of a calculator plugin system, and examines JavaScript and CSS sandboxing techniques to ensure plugin security in web applications.
Plugins let developers extend functionality, create product ecosystems, and reduce maintenance costs, but they also add unrelated code, increase system complexity, and raise security challenges.
When Should You Use Plugins?
Introduce plugins only when the following scenarios apply: ensuring core stability via a microkernel architecture, exposing capabilities for users to solve specific problems (e.g., Webpack, Figma), building a platform for third‑party extensions, or adapting to rapidly changing business requirements.
Atom’s decline is often blamed on its plugin system, which slowed performance and introduced security risks.
Design Principles
The simplest solution is usually the best. A good framework makes the wrong thing hard to do, guiding developers toward the right approach.
A good framework or a good architecture makes it hard to do the wrong thing. A lot of people will always go for the easiest way to get something done. So as a systems designer, your job is to make sure that the easiest thing to do is the right thing. And once you get that, the whole system gets more maintainable.
Abstraction of Change and Stability
Separate the immutable core from mutable extensions. The core remains stable, while plugins handle the parts that change, preventing plugin modifications from affecting the core.
SOLID Design Principles
SRP – Single Responsibility Principle.
OCP – Open/Closed Principle.
LSP – Liskov Substitution Principle.
ISP – Interface Segregation Principle.
DIP – Dependency Inversion Principle.
How to Design a Plugin System
Goal: design a simple, flexible, and safe plugin system.
Core Concepts
Key building blocks include:
Hook : points where plugins can be invoked (e.g., Webpack hooks).
Plugin Interface : a contract that plugins must implement.
Plugin Loader : registers plugins, either plugin‑driven (plugins self‑register) or system‑driven (system discovers and loads plugins).
Hook Example
compiler.hooks.beforeCompile.tapAsync("MyPlugin", (params, callback) => {
params["MyPlugin - data"] = "important stuff my plugin will use later";
callback();
});Plugin Interface Example
class HelloWorldPlugin {
apply(compiler) {
compiler.hooks.done.tap("Hello World Plugin", (stats) => {
console.log("Hello World!");
});
}
}
module.exports = HelloWorldPlugin;Plugin Loader
Plugin‑driven : plugins call a registration API (e.g., Vue.use).
System‑driven : the system scans a directory or configuration and loads plugins (e.g., Webpack).
Developing a Plugin System
We start with a simple calculator and then add plugin support.
Step 1 – Basic Calculator
class Calculator {
currentValue: number = 0;
setValue(newValue: number): void { this.currentValue = newValue; console.log(this.currentValue); }
plus(addend: number): void { this.setValue(this.currentValue + addend); }
minus(subtrahend: number): void { this.setValue(this.currentValue - subtrahend); }
}
const calculator = new Calculator();
calculator.setValue(3); // => 3
calculator.plus(3); // => 6
calculator.minus(2); // => 4Step 2 – Register Plugins
type IExec = (currentValue: number, operand?: number) => number;
interface IPlugin { name: string; exec: IExec; }
class Calculator {
private plugins: { [name: string]: IExec } = {};
constructor(private currentValue: number = 0) {}
register(plugin: IPlugin) { this.plugins[plugin.name] = plugin.exec; }
press(operation: string, operand?: number) {
const exec = this.plugins[operation];
if (typeof exec !== 'function') {
throw Error(`${operation} operation not supported yet!`);
}
this.setValue(exec(this.currentValue, operand));
}
private setValue(newValue: number) { this.currentValue = newValue; console.log(this.currentValue); }
}
const calculator = new Calculator(3);
calculator.press('plus', 2); // throws because plugin not registered yetStep 3 – Implement Plugins
// plus plugin
const plus: IPlugin = {
name: "plus",
exec: (currentValue, addend) => {
if (!addend) { throw Error("addend is required!"); }
return currentValue + addend;
},
};
// minus plugin
const minus: IPlugin = {
name: "minus",
exec: (currentValue, subtrahend) => {
if (!subtrahend) { throw Error("subtrahend is required!"); }
return currentValue - subtrahend;
},
};
const calculator = new Calculator(3);
calculator.register(plus);
calculator.press("plus", 2); // => 5At this point the calculator core only handles registration and execution, while each operation lives in its own plugin, following DIP and keeping the core stable.
Plugin Security
Popular approaches use sandboxing to isolate third‑party code.
JS Sandbox – Iframe
An iframe with the sandbox attribute provides strong isolation. Example:
<!-- All restrictions -->
<iframe src="xxx" sandbox=""></iframe>
<!-- Allow scripts, forms, same‑origin -->
<iframe src="xxx" sandbox="allow-scripts,allow-forms,allow-same-origin"></iframe>Limitations include message size via postMessage and difficulty integrating plugins that need tighter coupling.
JS Sandbox – with + Proxy
Combine a whitelist with with and Proxy to hide globals:
const whitelist = { window: undefined, document: undefined };
const scopeProxy = new Proxy(whitelist, {
get(target, prop) { return prop in target ? target[prop] : undefined; },
set(target, name, value) { if (Object.keys(whitelist).includes(name)) { whitelist[name] = value; return true; } return false; }
});
function sandBoxingEval(scopeProxy, userCode) {
with (scopeProxy) { eval(userCode); }
}
const code = `
console.log(window); // undefined
window.aa = 123; // error
`;
sandBoxingEval(scopeProxy, code);Even with a whitelist, prototypes can be accessed via ({}).constructor, so additional safeguards are needed.
JS Sandbox – Realms (Stage‑2 proposal)
Realms create a separate global environment:
const red = new Realm();
globalThis.someValue = 1;
red.evaluate("globalThis.someValue = 2"); // only affects the realm
console.assert(globalThis.someValue === 1);Not yet production‑ready, but the idea can be mimicked with existing features.
CSS Sandbox
Namespace
Use unique class prefixes (e.g., Ant Design, iView) to avoid style collisions.
Scoped CSS (Vue)
Apply the scope attribute to limit CSS to a component.
Shadow DOM
Encapsulate plugin markup and styles in a shadow root, preventing leakage.
const div = document.createElement("div");
const shadowRoot = div.attachShadow({ mode: "closed" });
const pluginDom = getPluginDom();
shadowRoot.appendChild(pluginDom);Summary
The article first discusses plugin use‑cases and when they are justified. It then presents fundamental design principles (simplicity, abstraction of change, SOLID) and shows a concrete plugin‑system implementation using a calculator example. Finally, it highlights security concerns and surveys JavaScript and CSS sandboxing techniques (iframe, with+proxy, Realms, namespace, scoped CSS, Shadow DOM) to help you build safe, extensible web plugins.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
