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.
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
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.
Agent definition
nodejs-security.mdNode.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
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)Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

