Skip to content
Development
Agent

nodejs-security

Secure-by-default patterns for Node.js backend applications. Each section shows what correct code looks like and why it matters. Load this reference when the task involves security, auth, injection, XSS, CSRF, SSRF, prototype pollution, or any vulnerability-related code.

From plugin
vexjoy-agent
449198 skills198 agents12 commands83 hooks

Claude Code

$ npx -y skills add notque/vexjoy-agent --agent claude-code

Agent definition

nodejs-security.md

Node.js Secure Implementation Patterns

Secure-by-default patterns for Node.js backend applications. Each section shows what correct code looks like and why it matters. Load this reference when the task involves security, auth, injection, XSS, CSRF, SSRF, prototype pollution, or any vulnerability-related code.

---

Use execFile Instead of exec for Process Spawning

Use `execFile` or `spawn` (with `shell: false`, the default) for subprocess calls. These pass arguments as separate argv entries without invoking a shell.

import { execFile, execFileSync, spawn } from 'child_process';

// Correct: execFile passes args directly to the binary
execFile('git', ['clone', '--', userUrl], (err, stdout) => {
    if (err) console.error('clone failed:', err.message);
});

// Correct: spawn with default shell: false
const child = spawn('convert', [userInput, 'output.png']);

// Correct: execFileSync for synchronous operations
const output = execFileSync('git', ['log', '--oneline', '-5']);

**Why this matters**: `exec` and `execSync` always invoke a shell, where metacharacters (`;`, `|`, `&`, `$()`) are interpreted. On Windows, `spawn`/`execFile` targeting `.bat`/`.cmd` files implicitly route through `cmd.exe` regardless of the `shell` option (CVE-2024-27980, fixed in Node 18.20.0 / 20.12.0 / 21.7.0).

**Detection**:

rg -n '\bexec\(|\bexecSync\(' . --type ts --type js
rg -n 'shell:\s*true' . --type ts --type js

---

Prevent Prototype Pollution With Safe Object Handling

Use Zod or similar schema validation before merging user input into objects. For key-value stores with user-controlled keys, use `Map` or `Object.create(null)`.

import { z } from 'zod';

// Correct: validate shape with Zod before any merge
const ConfigSchema = z.object({
    theme: z.enum(['light', 'dark']).optional(),
    language: z.string().max(5).optional(),
});
const validated = ConfigSchema.parse(req.body);
const merged = { ...defaultConfig, ...validated };

// Correct: use Map for user-controlled keys
const userSettings = new Map<string, unknown>();
for (const [key, value] of Object.entries(validatedInput)) {
    userSettings.set(key, value);
}

// Correct: prototype-free object for lookups
const lookup = Object.create(null) as Record<string, string>;

**Why this matters**: Deep-merging `req.body` into objects without filtering `__proto__`, `constructor`, or `prototype` keys pollutes `Object.prototype`. Downstream code that reads properties from shared objects (template engines, HTTP clients, auth checks) picks up attacker-injected values. CVE-2019-10744 (lodash `defaultsDeep`), CVE-2019-19919 (Handlebars compile-time RCE), and CVE-2026-40175 (axios header injection bypassing IMDSv2) demonstrate the full attack chain.

**Detection**:

rg -n '_\.(merge|defaultsDeep|set|setWith)\(.*req\.' . --type ts --type js
rg -n 'Object\.assign\(.*req\.' . --type ts --type js
rg -n 'for\s*\(.*Object\.keys\(.*req\.' . --type ts --type js

---

Order Express Middleware for Auth Before Routes

Mount authentication middleware before route handlers. Middleware executes in registration order; auth mounted after a route leaves that route unprotected.

import express from 'express';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';

const app = express();

// 1. Security headers first
app.use(helmet());

// 2. Body parsing
app.use(express.json({ strict: true }));

// 3. Rate limiting
app.use('/api/', rateLimit({
    windowMs: 60_000,
    max: 100,
    standardHeaders: true,
}));

// 4. Auth middleware BEFORE routes
app.use('/api/', authMiddleware);

// 5. Routes come last
app.use('/api/users', userRouter);
app.use('/api/invoices', invoiceRouter);

// 6. Error handler at the end
app.use(errorHandler);

**Why this matters**: Express processes middleware in the order registered. If a route is mounted before auth middleware, requests to that route bypass authentication entirely. This is the Express-equivalent of "forced browsing."

**Detection**:

rg -n 'app\.use\(' . --type ts --type js | head -30

---

Pin JWT Algorithms and Verify Claims

Always pass an explicit `algorithms` allowlist when verifying JWTs. Verify `exp`, `aud`, and `iss` claims. Use short-lived access tokens with refresh tokens for revocability.

import jwt from 'jsonwebtoken';

// Correct: pin algorithm, verify standard claims
const payload = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],        // Pin to a single algorithm family
    audience: 'api.example.com',  // Verify audience
    issuer: 'auth.example.com',   // Verify issuer
});

// Correct: sign with explicit algorithm and short expiry
const token = jwt.sign(
    { sub: user.id, role: user.role },
    privateKey,
    {
        algorithm: 'RS256',
        expiresIn: '15m',         // Short-lived access token
        audience: 'api.example.com',
        issuer: 'auth.example.com',
    },
);

**Why this matters**: `jwt.verify(token, key)` without `algorithms` allows the attacker to choose the algorithm. CVE-2022-23540 and CVE-2022-23541 (jsonwebtoken < 9.0.0) allowed `alg: none` verification and RS-to-HS key confusion. CVE-2022-29217 (PyJWT) had the same class of bug. Never include `"none"` in the algorithms list. Never mix HS and RS algorithms with a single key.

**Detection**:

rg -n 'jwt\.verify\(' . --type ts --type js | rg -v 'algorithms'
rg -n 'jwt\.decode\(' . --type ts --type js
rg -n "algorithms.*none" . --type ts --type js

---

Validate Outbound URLs to Prevent SSRF

Resolve hostnames to IPs and validate against private/internal ranges before making outbound requests. Disable redirect following or re-validate on each hop.

import { lookup } from 'dns/promises';
import ipaddr from 'ipaddr.js';

const DISALLOWED_RANGES = ['private', 'linkLocal', 'loopback', 'uniqueLocal', 'unspecified'];

async function safeFetch(userUrl: string): Promise<Response> {
    const url = new URL(userUrl)
Read more
@notque
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. VexJoy Agent connects plain-English requests to specialist agents, skills, and workflows. /do selects the knowledge and tools needed for your task.

44964 skills198 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
449
Star history of notque/vexjoy-agent on GitHub, a chart by star-history.com

Other agents on vexjoy-agent.

modules

modules

Agent

**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes…

@notque@notqueInstall Agent
testing

testing

Agent

**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**:…

@notque@notqueInstall Agent
communication-patterns

communication-patterns

Agent

**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what…

@notque@notqueInstall Agent

More agents from other plugins.

a11y-architect
Auto-invoked

a11y-architect

Agent

Accessibility Architect specializing in WCAG 2.2 compliance for Web and Native platforms. Use PROACTIVELY when designing UI…

ecc13d293k
@affaan-m@affaan-mInstall Agent
analyze-pass-impact

analyze-pass-impact

Agent

Analyzes how a specific topic affects a group of compiler passes. Used by the /plan-update skill to parallelize research across…

react2d252k
@facebook@facebookInstall Agent
orchestrator

orchestrator

Agent

- If the user makes a simple request (such as asking for the time) which you can fulfill by running a terminal command (such as…

codex2d135k
@openai@openaiInstall Agent
cavecrew-builder

cavecrew-builder

Agent

Surgical 1-2 file edit. Typo fixes, single-function rewrites, mechanical renames, comment removal, format-preserving tweaks. Hard…

caveman3d119k
@juliusbrussee@juliusbrusseeInstall Agent

Questions about nodejs-security.

Which coding agents can use nodejs-security?
nodejs-security installs in Claude Code and Cursor: each loads subagents from .claude/agents, so copy the file there. Flowy's auto-invocation runs in Claude Code only.