11 views
Plugins are how product teams ship extensibility and how supply-chain malware gets a free process. Node’s experimental permission model (--permission) finally lets you treat a plugin worker like a jail: declare the paths and hosts it may touch, fail closed on everything else, and keep the parent process’s secrets out of reach. This is not a replacement for container isolation — it is the innermost belt when a plugin already runs inside your address space.
⚡ TL;DR: Boot plugin workers with--permission --allow-fs-read=... --allow-fs-write=... --allow-net=...(and deny child_process by default); never grant the parent process’s home or.envtrees; wrapprocess.permissionchecks in your plugin loader so missing flags fail CI; pair with Agent Tool Allowlists: Least Privilege for Filesystem and Shell Access for the outer agent sandbox. MeasureERR_ACCESS_DENIEDrates in staging before prod.
Node Permission Model: production guidance
Your API process holds DB creds, IAM role credentials via the instance metadata path, and customer data in memory. A compromisedrequire()d plugin — or a WASM plugin that shells out — inherits all of that unless you split processes and shrink the child’s capability set.
typescript
// src/plugin-host.ts — spawn with an explicit allowlist
import { Worker } from "node:worker_threads";
import { fileURLToPath } from "node:url";
const PLUGIN_ROOT = fileURLToPath(new URL("../plugins/sandbox/", import.meta.url));
export function startPluginWorker(entry: string) {
// ✅ Child gets permission flags; parent does not need them
return new Worker(entry, {
execArgv: [
"--permission",
`--allow-fs-read=${PLUGIN_ROOT}`,
`--allow-fs-read=${process.execPath}`,
`--allow-fs-write=${PLUGIN_ROOT}/tmp`,
"--allow-net=api.partner.example.com",
// omit --allow-child-process → denied
],
workerData: { pluginRoot: PLUGIN_ROOT },
});
}
typescript
// ❌ Same process, full FS — "we'll review plugins carefully"
import { createRequire } from "node:module";
const req = createRequire(import.meta.url);
export const plugin = req("./plugins/community/do-stuff.js");
Map capabilities to plugin contracts
Every plugin declares a manifest of paths and hosts. The host validates the manifest against an org policy, then translates it intoexecArgv. Drift between manifest and flags is how you get silent over-permission.
typescript
Cross-check the same philosophy used for AI tool runners in Secure AI Sandboxes: Ephemeral ECS Tasks for Agent Tool Execution — declare, allowlist, deny by default.
// src/plugin-manifest.ts
export type PluginManifest = {
id: string;
fsRead: string[];
fsWrite: string[];
net: string[];
};
const ORG_DENY_NET = new Set(["169.254.169.254", "metadata.google.internal"]);
export function toExecArgv(m: PluginManifest, root: string): string[] {
for (const p of [...m.fsRead, ...m.fsWrite]) {
if (!p.startsWith(root)) throw new Error(`path_outside_root:${p}`);
}
for (const h of m.net) {
if (ORG_DENY_NET.has(h) || h.includes("*")) throw new Error(`net_denied:${h}`);
}
return [
"--permission",
...m.fsRead.map((p) => `--allow-fs-read=${p}`),
...m.fsWrite.map((p) => `--allow-fs-write=${p}`),
...m.net.map((h) => `--allow-net=${h}`),
];
}
Fail closed in CI and on boot
If someone deploys without--permission, your “secure” plugin host is theater. Assert the flag at process start and in the image entrypoint test.
typescript
// src/assert-permission.ts
export function assertPermissionModelEnabled() {
if (typeof process.permission === "undefined") {
throw new Error("NODE_PERMISSION_MODEL_REQUIRED");
}
const sneaky = "/etc/passwd";
if (process.permission.has("fs.read", sneaky)) {
throw new Error("over_permissioned_fs_read");
}
}
yaml
# .github/workflows/plugin-host.yml
- name: Permission flag smoke
run: |
node --permission --allow-fs-read=$PWD/plugins/sandbox \
--eval "require('./dist/assert-permission.js').assertPermissionModelEnabled()"
Observability for denials
TreatERR_ACCESS_DENIED as a first-class signal. A spike after a plugin release usually means the manifest is wrong — or the plugin is probing.
typescript
// src/plugin-worker-entry.ts
process.on("uncaughtException", (err: NodeJS.ErrnoException) => {
if (err?.code === "ERR_ACCESS_DENIED") {
metrics.increment("plugin.permission_denied", {
plugin: process.env.PLUGIN_ID,
});
}
throw err;
});
Production rollout pattern
- Run plugin workers under
--permissionin staging with verbose denial logs. - Tighten manifests until denials are only malicious or unexpected paths.
- Block deploys that omit the flag via entrypoint assert.
- Keep network allowlists hostname-exact; never
*.amazonaws.com. - Still run the worker in a separate cgroup/task — permissions are defense in depth.
Closing checklist
✅ Dos– ✅ Spawn plugins in Workers/child processes with
--permission– ✅ Derive flags from a reviewed manifest under a fixed root
– ✅ Assert
process.permission in CI and boot– ✅ Meter
ERR_ACCESS_DENIED per plugin id– ✅ Deny child_process and metadata IPs by default ❌ Don’ts
– ❌ Don’t
require() untrusted plugins into the API process– ❌ Don’t allow-read the repo root, home, or
/var/run/secrets– ❌ Don’t use net wildcards in production
– ❌ Don’t treat the permission model as a substitute for IAM/task roles
– ❌ Don’t ignore denial spikes after plugin upgrades
Related reading
- Agent Tool Allowlists: Least Privilege for Filesystem and Shell Access
- Secure AI Sandboxes: Ephemeral ECS Tasks for Agent Tool Execution
- Claude Code Hooks: Gate Risky Shell Commands Before CI Runs
- AI Code Review Bots: IAM, Secrets, and Least-Privilege Pipelines
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- Css Animation Performance: Performance-Optimized Motion for Modern UIs
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
