@schalkneethling/calavera-skill-frontend-security 0.2.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,352 @@
1
+ # JWT Security Reference
2
+
3
+ ## Common Vulnerabilities
4
+
5
+ ### None Algorithm Attack
6
+
7
+ Attack: Attacker changes algorithm to "none" to bypass signature verification.
8
+
9
+ ```javascript
10
+ // VULNERABLE - explicitly accepts unsigned tokens
11
+ const decoded = jwt.verify(token, null, { algorithms: ["none"] });
12
+
13
+ // SECURE - explicitly specify allowed algorithms
14
+ const decoded = jwt.verify(token, secret, {
15
+ algorithms: ["HS256"], // Only accept HS256
16
+ });
17
+ ```
18
+
19
+ ### Algorithm Confusion Attack
20
+
21
+ Attack: Switching from RS256 to HS256 using public key as secret.
22
+
23
+ ```javascript
24
+ // VULNERABLE - disables the asymmetric key-type safety check for compatibility
25
+ const decoded = jwt.verify(token, publicKey, {
26
+ allowInvalidAsymmetricKeyTypes: true,
27
+ });
28
+
29
+ // SECURE - specify expected algorithm
30
+ const decoded = jwt.verify(token, publicKey, {
31
+ algorithms: ["RS256"], // Only accept RS256
32
+ });
33
+ ```
34
+
35
+ ### Weak Secret
36
+
37
+ Attack: Brute-force weak HMAC secrets.
38
+
39
+ ```javascript
40
+ // VULNERABLE - weak secret
41
+ const token = jwt.sign(payload, "password123");
42
+
43
+ // SECURE - stable secret provisioned by a secret manager (256+ bits)
44
+ const crypto = require("node:crypto");
45
+ const fs = require("node:fs");
46
+ if (typeof process.env.JWT_SECRET_BASE64 !== "string") throw new Error("Missing JWT secret");
47
+ const secret = Buffer.from(process.env.JWT_SECRET_BASE64, "base64");
48
+ if (secret.length < 32) throw new Error("JWT secret must contain at least 256 bits");
49
+ const token = jwt.sign(payload, secret);
50
+
51
+ // Or use RSA keys
52
+ const privateKey = fs.readFileSync("private.pem");
53
+ const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });
54
+ ```
55
+
56
+ ## Token Sidejacking Prevention
57
+
58
+ For browser-held bearer tokens, a hardened fingerprint cookie can reduce reuse
59
+ of a stolen token. Treat it as defense in depth, not a substitute for short
60
+ lifetimes, XSS prevention, revocation, or keeping tokens out of browser storage
61
+ when the architecture allows it.
62
+
63
+ ```javascript
64
+ const crypto = require("crypto");
65
+
66
+ // Generate fingerprint on login
67
+ function generateFingerprint() {
68
+ return crypto.randomBytes(32).toString("hex");
69
+ }
70
+
71
+ // Create token with fingerprint hash
72
+ function createToken(userId, fingerprint) {
73
+ const fingerprintHash = crypto.createHash("sha256").update(fingerprint).digest("hex");
74
+
75
+ return jwt.sign({ sub: userId, fph: fingerprintHash }, secret, { expiresIn: "15m" });
76
+ }
77
+
78
+ // Set fingerprint as httpOnly cookie
79
+ res.cookie("__Secure-Fgp", fingerprint, {
80
+ httpOnly: true,
81
+ secure: true,
82
+ sameSite: "Strict",
83
+ maxAge: 15 * 60 * 1000, // Match token expiry
84
+ });
85
+
86
+ // Validate both token and fingerprint
87
+ function validateToken(token, fingerprintCookie, { requireFingerprint = false } = {}) {
88
+ const decoded = jwt.verify(token, secret, { algorithms: ["HS256"] });
89
+
90
+ if (!requireFingerprint) return decoded;
91
+ if (typeof fingerprintCookie !== "string") throw new Error("Missing fingerprint cookie");
92
+
93
+ const fingerprintHash = crypto.createHash("sha256").update(fingerprintCookie).digest("hex");
94
+
95
+ if (decoded.fph !== fingerprintHash) {
96
+ throw new Error("Invalid fingerprint");
97
+ }
98
+
99
+ return decoded;
100
+ }
101
+ ```
102
+
103
+ ## Token Storage and Browser Architecture
104
+
105
+ Choose token handling from the application architecture. Do not present
106
+ Web Storage as a universal default.
107
+
108
+ ### Traditional Web App or BFF: httpOnly Cookie
109
+
110
+ Prefer server-owned sessions or a backend-for-frontend (BFF) that stores tokens
111
+ server-side and sends only `httpOnly`, `Secure`, SameSite cookies to the
112
+ browser. Cookie-based auth needs CSRF protection on state-changing requests;
113
+ SameSite is useful defense in depth but does not replace tokens plus
114
+ Origin/Referer checks for sensitive operations.
115
+
116
+ ```javascript
117
+ // Server sets cookie
118
+ res.cookie("token", jwt, {
119
+ httpOnly: true, // Not accessible via JavaScript
120
+ secure: true, // HTTPS only
121
+ sameSite: "Strict", // CSRF protection
122
+ maxAge: 15 * 60 * 1000,
123
+ });
124
+
125
+ // Need CSRF protection with cookie-based auth
126
+ ```
127
+
128
+ ### SPA: In-Memory Access Token
129
+
130
+ For browser-only SPAs, prefer short-lived access tokens kept in memory, refresh
131
+ token rotation where appropriate, and a clear re-authentication path. Avoid
132
+ long-lived bearer tokens in Web Storage. If refresh tokens are issued to browser
133
+ clients, rotate them, expire them, detect reuse, and avoid storing them in
134
+ `localStorage` or other long-lived JavaScript-readable storage.
135
+
136
+ ```javascript
137
+ let accessToken = null;
138
+ const trustedApiOrigin = "https://api.example.com";
139
+
140
+ export function setAccessToken(token) {
141
+ accessToken = token;
142
+ }
143
+
144
+ export async function apiFetch(url, options = {}) {
145
+ const target = new URL(url, trustedApiOrigin);
146
+ if (target.origin !== trustedApiOrigin) {
147
+ throw new Error("Refusing to send bearer token to an untrusted origin");
148
+ }
149
+
150
+ return fetch(target.href, {
151
+ ...options,
152
+ headers: {
153
+ ...options.headers,
154
+ ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
155
+ },
156
+ });
157
+ }
158
+ ```
159
+
160
+ ### Constrained Fallback: sessionStorage + Fingerprint Cookie
161
+
162
+ `sessionStorage` is a last-resort compromise for browser-held bearer tokens, not
163
+ a security improvement over in-memory storage. It is readable by XSS. Use it
164
+ only when the architecture requires browser-held tokens and the risk is
165
+ explicitly accepted. Keep token lifetimes short, bind tokens to an `httpOnly`
166
+ fingerprint cookie when possible, and clear tokens on logout.
167
+
168
+ ```javascript
169
+ sessionStorage.setItem("token", jwt);
170
+ sessionStorage.removeItem("token");
171
+ ```
172
+
173
+ ### Avoid: localStorage
174
+
175
+ ```javascript
176
+ // VULNERABLE - persists after browser close, accessible to XSS
177
+ localStorage.setItem("token", jwt); // Not recommended
178
+ ```
179
+
180
+ ## Token Expiration
181
+
182
+ The refresh-token example is a server-side sketch. For browser clients, do not
183
+ pair it with long-lived refresh tokens stored in Web Storage; prefer a BFF,
184
+ server-side session, or refresh-token rotation with storage and replay controls
185
+ chosen for the client architecture.
186
+
187
+ ```javascript
188
+ // Short-lived access tokens (15-60 minutes)
189
+ const accessToken = jwt.sign(payload, secret, { expiresIn: "15m" });
190
+
191
+ // Use Redis or another shared durable store in production. Atomic consume is
192
+ // required so two requests cannot rotate the same refresh token.
193
+ const activeRefreshTokens = new Map();
194
+
195
+ function issueRefreshToken(userId) {
196
+ const jti = crypto.randomUUID();
197
+ const token = jwt.sign({ sub: userId, type: "refresh", jti }, refreshSecret, {
198
+ expiresIn: "7d",
199
+ });
200
+ activeRefreshTokens.set(jti, userId);
201
+ return token;
202
+ }
203
+
204
+ const refreshToken = issueRefreshToken(userId);
205
+
206
+ // Refresh endpoint
207
+ app.post("/refresh", async (req, res) => {
208
+ const { refreshToken } = req.body;
209
+
210
+ try {
211
+ const decoded = jwt.verify(refreshToken, refreshSecret, {
212
+ algorithms: ["HS256"],
213
+ });
214
+
215
+ if (decoded.type !== "refresh") {
216
+ throw new Error("Invalid token type");
217
+ }
218
+
219
+ if (!decoded.jti || activeRefreshTokens.get(decoded.jti) !== decoded.sub) {
220
+ throw new Error("Token revoked");
221
+ }
222
+
223
+ // In production, replace this Map operation with an atomic get-and-delete.
224
+ activeRefreshTokens.delete(decoded.jti);
225
+
226
+ // Issue new access token
227
+ const newAccessToken = jwt.sign({ sub: decoded.sub, jti: crypto.randomUUID() }, secret, {
228
+ expiresIn: "15m",
229
+ });
230
+ const newRefreshToken = issueRefreshToken(decoded.sub);
231
+
232
+ res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
233
+ } catch (error) {
234
+ res.status(401).json({ error: "Invalid refresh token" });
235
+ }
236
+ });
237
+ ```
238
+
239
+ ## Token Revocation
240
+
241
+ JWTs are stateless, so revocation requires additional mechanisms:
242
+
243
+ ```javascript
244
+ // Denylist approach
245
+ const revokedTokens = new Map(); // Use Redis in production
246
+
247
+ function revokeToken(token) {
248
+ const decoded = jwt.verify(token, secret, { algorithms: ["HS256"] });
249
+ const tokenId = decoded.jti;
250
+ const expiry = decoded.exp * 1000;
251
+
252
+ if (!tokenId || !expiry) {
253
+ throw new Error("Token missing required revocation claims");
254
+ }
255
+
256
+ // Store until token expires
257
+ revokedTokens.set(tokenId, expiry);
258
+
259
+ // Clean up expired entries periodically
260
+ setTimeout(() => revokedTokens.delete(tokenId), Math.max(0, expiry - Date.now()));
261
+ }
262
+
263
+ function isTokenRevoked(decoded) {
264
+ return Boolean(decoded.jti) && revokedTokens.has(decoded.jti);
265
+ }
266
+
267
+ // Include jti (JWT ID) in tokens
268
+ const token = jwt.sign({ sub: userId, jti: crypto.randomUUID() }, secret, { expiresIn: "15m" });
269
+ ```
270
+
271
+ ## Token Information Disclosure
272
+
273
+ JWTs are base64-encoded, not encrypted. Sensitive data is visible.
274
+
275
+ ```javascript
276
+ // VULNERABLE - sensitive data in payload
277
+ const token = jwt.sign(
278
+ {
279
+ sub: userId,
280
+ ssn: "123-45-6789", // Visible to anyone!
281
+ salary: 100000,
282
+ },
283
+ secret,
284
+ );
285
+
286
+ // SECURE - minimal claims
287
+ const token = jwt.sign(
288
+ {
289
+ sub: userId,
290
+ role: "user",
291
+ },
292
+ secret,
293
+ );
294
+
295
+ // If sensitive claims are unavoidable, use JWE or keep the data server-side.
296
+ const encryptedToken = encrypt(token, encryptionKey);
297
+ ```
298
+
299
+ ## Validation Middleware
300
+
301
+ This middleware validates bearer tokens after they reach an API. It does not
302
+ recommend where the browser should store those tokens; use the storage guidance
303
+ above before deciding whether a browser should send an `Authorization` header at
304
+ all.
305
+
306
+ ```javascript
307
+ function authenticateToken(req, res, next) {
308
+ // Get token from header
309
+ const authHeader = req.headers.authorization;
310
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined;
311
+
312
+ if (!token) {
313
+ return res.status(401).json({ error: "Token required" });
314
+ }
315
+
316
+ try {
317
+ // Validate token and fingerprint when using sidejacking protection
318
+ const fingerprint = req.cookies["__Secure-Fgp"];
319
+ const decoded = validateToken(token, fingerprint);
320
+
321
+ // Check revocation
322
+ if (isTokenRevoked(decoded)) {
323
+ throw new Error("Token revoked");
324
+ }
325
+
326
+ req.user = decoded;
327
+ next();
328
+ } catch (error) {
329
+ if (error.name === "TokenExpiredError") {
330
+ return res.status(401).json({ error: "Token expired" });
331
+ }
332
+ return res.status(403).json({ error: "Invalid token" });
333
+ }
334
+ }
335
+ ```
336
+
337
+ ## Security Checklist
338
+
339
+ - [ ] Explicit algorithm specification (never auto-detect)
340
+ - [ ] Strong secret (256+ bits) or RSA keys
341
+ - [ ] Short expiration times (15-60 minutes for access tokens)
342
+ - [ ] Fingerprint cookie considered for browser-held bearer tokens where appropriate
343
+ - [ ] Validate issuer (iss) and audience (aud) claims
344
+ - [ ] Implement token revocation mechanism
345
+ - [ ] No sensitive data in payload
346
+ - [ ] Choose storage based on architecture: server-side session/BFF, in-memory SPA token, or constrained sessionStorage fallback
347
+ - [ ] Refresh tokens are rotated, replay-detected, and not kept in long-lived Web Storage
348
+ - [ ] Avoid long-lived bearer tokens in Web Storage
349
+ - [ ] Protect cookie-based auth with CSRF defenses
350
+ - [ ] Use HTTPS only
351
+
352
+ OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html
@@ -0,0 +1,325 @@
1
+ # Node.js and NPM Security Reference
2
+
3
+ ## NPM Dependency Security
4
+
5
+ ### Audit Commands
6
+
7
+ ```bash
8
+ # Check for vulnerabilities
9
+ npm audit
10
+
11
+ # Preview the proposed changes and lockfile diff first, then run `npm audit fix`
12
+ npm audit fix
13
+
14
+ # Force fix can apply breaking direct or transitive upgrades. Use only after
15
+ # reviewing release notes, dependency graphs, lockfile diffs, and test results.
16
+ npm audit fix --force
17
+
18
+ # Generate detailed report
19
+ npm audit --json > audit-report.json
20
+ ```
21
+
22
+ ### Lockfile Enforcement
23
+
24
+ ```bash
25
+ # Always use lockfile in CI/CD
26
+ npm ci # Instead of npm install
27
+
28
+ # Verify lockfile integrity
29
+ npm ci --ignore-scripts # Safer for first run
30
+ ```
31
+
32
+ ### Dependency Update Review
33
+
34
+ Treat automated dependency updates as reviewable changes, not background
35
+ maintenance. Dependabot or Renovate PRs are useful because they isolate the
36
+ package, lockfile, release notes, provenance or publisher signals where
37
+ available, and CI result for each update. Review dependency diffs carefully for
38
+ new install scripts, ownership changes, unexpected transitive churn, native
39
+ build steps, and broad permission or runtime changes.
40
+
41
+ Run audits in CI and scheduled automation so ordinary installs are predictable.
42
+ Use `npm audit fix --force` only when the breaking-change and transitive-update
43
+ effects are understood.
44
+
45
+ ### Package.json Security
46
+
47
+ Avoid package lifecycle scripts that fetch or audit during ordinary installs.
48
+ Install-time `npx` expands supply-chain trust, and `postinstall` audits make
49
+ local installs depend on network and registry behavior. Prefer pinned
50
+ devDependencies, package-manager-native overrides, CI checks, and scheduled
51
+ dependency automation.
52
+
53
+ ```json
54
+ {
55
+ "overrides": {
56
+ "vulnerable-package": "^2.0.0"
57
+ },
58
+ "scripts": {
59
+ "audit:ci": "npm audit --audit-level=high"
60
+ }
61
+ }
62
+ ```
63
+
64
+ ## Dangerous Functions
65
+
66
+ ### Code Execution
67
+
68
+ ```javascript
69
+ // DANGEROUS - never use with user input
70
+ eval(userInput);
71
+ new Function(userInput);
72
+ vm.runInThisContext(userInput);
73
+ require(userInput);
74
+
75
+ // DANGEROUS - dynamic module loading with user-controlled specifiers
76
+ await import(userInput);
77
+
78
+ // SAFER - map user choices to fixed module specifiers
79
+ const allowedModules = new Map([["csv", "./parsers/csv.js"]]);
80
+ const modulePath = allowedModules.get(userInput);
81
+ if (!modulePath) throw new Error("Invalid module");
82
+ await import(modulePath);
83
+ ```
84
+
85
+ ### Child Process Injection
86
+
87
+ ```javascript
88
+ // DANGEROUS - command injection
89
+ const { exec } = require("child_process");
90
+ exec(`ls ${userInput}`); // Shell injection
91
+
92
+ // SAFER - use execFile with arguments array and no shell
93
+ const { execFile } = require("child_process");
94
+ execFile("ls", ["--", userInput], { shell: false }, callback);
95
+
96
+ // SAFEST - avoid shelling out when a library API can do the work.
97
+ // If a command is necessary, allowlist the command and argument shapes.
98
+ const { spawn } = require("child_process");
99
+ const allowedFormats = new Set(["json", "text"]);
100
+ if (!allowedFormats.has(userSelectedFormat)) {
101
+ throw new Error("Invalid format");
102
+ }
103
+ spawn("tool", ["--format", userSelectedFormat, "--", userPath], {
104
+ shell: false,
105
+ cwd: "/srv/app",
106
+ env: { PATH: "/usr/bin:/bin" },
107
+ });
108
+ ```
109
+
110
+ Argument arrays prevent shell metacharacter expansion, but they do not make
111
+ user-controlled arguments safe automatically. Some programs treat values as
112
+ options, paths, patterns, or expressions. Use `--` before user-controlled path
113
+ arguments when supported, validate option values, set `cwd` and `env`
114
+ explicitly, run with least privilege, and treat command output as untrusted.
115
+
116
+ ### File System
117
+
118
+ ```javascript
119
+ const path = require("node:path");
120
+ const fs = require("node:fs");
121
+
122
+ // DANGEROUS - path traversal
123
+ const filePath = `/uploads/${userInput}`;
124
+
125
+ // Keep baseDir and its parent directories trusted and non-writable by users.
126
+ function safeReadFile(userInput, baseDir) {
127
+ const basePath = fs.realpathSync(baseDir);
128
+ const requestedPath = path.resolve(basePath, userInput);
129
+ const safePath = fs.realpathSync(requestedPath);
130
+ const relativePath = path.relative(basePath, safePath);
131
+
132
+ // Verify path is within allowed directory
133
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
134
+ throw new Error("Invalid file path");
135
+ }
136
+
137
+ // O_NOFOLLOW prevents a final-component symlink swap between validation and open.
138
+ // The trusted-directory requirement prevents intermediate path replacement.
139
+ const fd = fs.openSync(safePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
140
+ try {
141
+ return fs.readFileSync(fd);
142
+ } finally {
143
+ fs.closeSync(fd);
144
+ }
145
+ }
146
+ ```
147
+
148
+ ## Request Handling
149
+
150
+ ### Rate Limiting
151
+
152
+ The default memory store below is suitable only for a single-process development example. In
153
+ multi-process or multi-instance deployments, configure a shared store such as Redis and set
154
+ Express `trust proxy` only to the exact proxy topology so client IP keys cannot be spoofed.
155
+
156
+ ```javascript
157
+ const rateLimit = require("express-rate-limit");
158
+
159
+ const limiter = rateLimit({
160
+ windowMs: 15 * 60 * 1000, // 15 minutes
161
+ max: 100, // Limit each IP to 100 requests per window
162
+ message: "Too many requests",
163
+ standardHeaders: true,
164
+ legacyHeaders: false,
165
+ });
166
+
167
+ app.use(limiter);
168
+
169
+ // Stricter limits for auth endpoints
170
+ const authLimiter = rateLimit({
171
+ windowMs: 60 * 60 * 1000, // 1 hour
172
+ max: 5, // 5 attempts per hour
173
+ message: "Too many login attempts",
174
+ });
175
+
176
+ app.use("/api/login", authLimiter);
177
+ ```
178
+
179
+ ### Request Size Limits
180
+
181
+ ```javascript
182
+ const express = require("express");
183
+ const app = express();
184
+
185
+ // Limit JSON body size
186
+ app.use(express.json({ limit: "100kb" }));
187
+
188
+ // Limit URL-encoded body
189
+ app.use(express.urlencoded({ extended: true, limit: "100kb" }));
190
+
191
+ // Limit file uploads
192
+ const multer = require("multer");
193
+ const upload = multer({
194
+ limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
195
+ });
196
+ ```
197
+
198
+ ### Timeout Configuration
199
+
200
+ ```javascript
201
+ const server = app.listen(3000);
202
+
203
+ // Set timeouts
204
+ server.setTimeout(30000); // 30 seconds
205
+ server.keepAliveTimeout = 65000;
206
+ server.headersTimeout = 66000;
207
+ ```
208
+
209
+ ## Secure Headers
210
+
211
+ ```javascript
212
+ const helmet = require("helmet");
213
+
214
+ app.use(
215
+ helmet({
216
+ contentSecurityPolicy: {
217
+ directives: {
218
+ defaultSrc: ["'self'"],
219
+ scriptSrc: ["'self'"],
220
+ styleSrc: ["'self'"],
221
+ imgSrc: ["'self'", "data:", "https:"],
222
+ objectSrc: ["'none'"],
223
+ upgradeInsecureRequests: [],
224
+ },
225
+ },
226
+ hsts: {
227
+ maxAge: 31536000,
228
+ includeSubDomains: true,
229
+ preload: true,
230
+ },
231
+ referrerPolicy: { policy: "strict-origin-when-cross-origin" },
232
+ frameguard: { action: "deny" },
233
+ }),
234
+ );
235
+ ```
236
+
237
+ ## Error Handling
238
+
239
+ ```javascript
240
+ // Global error handler - don't expose details
241
+ app.use((err, req, res, next) => {
242
+ // Use a structured logger configured to redact credentials, request data,
243
+ // SQL, tokens, and PII. Emit only reviewed fields.
244
+ logger.error({
245
+ message: typeof err?.message === "string" ? err.message.slice(0, 256) : "Unhandled error",
246
+ stack: process.env.NODE_ENV === "development" ? err?.stack : undefined,
247
+ });
248
+
249
+ // Send generic message to client
250
+ res.status(500).json({
251
+ error: "An unexpected error occurred",
252
+ });
253
+ });
254
+
255
+ // Async error wrapper
256
+ const asyncHandler = (fn) => (req, res, next) => {
257
+ Promise.resolve(fn(req, res, next)).catch(next);
258
+ };
259
+
260
+ app.get(
261
+ "/data",
262
+ asyncHandler(async (req, res) => {
263
+ const data = await fetchData();
264
+ res.json(data);
265
+ }),
266
+ );
267
+ ```
268
+
269
+ ## Environment Variables
270
+
271
+ ```javascript
272
+ // NEVER commit secrets to code
273
+ // Use environment variables
274
+ const apiKey = process.env.API_KEY;
275
+
276
+ // Validate required env vars at startup
277
+ const required = ["API_KEY", "DB_URL", "SESSION_SECRET"];
278
+ required.forEach((varName) => {
279
+ if (!process.env[varName]) {
280
+ console.error(`Missing required env var: ${varName}`);
281
+ process.exit(1);
282
+ }
283
+ });
284
+ ```
285
+
286
+ ## Regex DoS Prevention
287
+
288
+ ```javascript
289
+ // DANGEROUS - evil regex (catastrophic backtracking)
290
+ const evilRegex = /^(a+)+$/;
291
+ evilRegex.test("aaaaaaaaaaaaaaaaaaaaaaaaaaa!"); // Hangs
292
+
293
+ // Heuristic only: safe-regex can have false positives/negatives for ReDoS
294
+ const safe = require("safe-regex");
295
+ if (!safe(userProvidedRegex)) {
296
+ throw new Error("Unsafe regex pattern");
297
+ }
298
+
299
+ // Preferred for untrusted patterns: use RE2 for guaranteed linear time
300
+ const RE2 = require("re2");
301
+ const pattern = new RE2(userProvidedRegex);
302
+ ```
303
+
304
+ ## NPM Security Checklist
305
+
306
+ - [ ] Run `npm audit` regularly and in CI/CD
307
+ - [ ] Use `npm ci` instead of `npm install` in CI
308
+ - [ ] Enable 2FA on npm account
309
+ - [ ] Use lockfiles and commit them
310
+ - [ ] Review new dependencies before installation
311
+ - [ ] Use `--ignore-scripts` for untrusted packages
312
+ - [ ] Set up automated vulnerability scanning such as Dependabot or Renovate
313
+ - [ ] Review dependency and lockfile diffs for risky scripts, publisher changes, and unexpected transitive churn
314
+ - [ ] Keep dependencies updated
315
+ - [ ] Avoid typosquatting by double-checking package names
316
+ - [ ] Use `npm-shrinkwrap.json` only when publishing a deployable app or CLI that must lock transitive dependencies; avoid it for libraries unless you intentionally want to constrain consumers
317
+
318
+ OWASP References:
319
+
320
+ - https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html
321
+ - https://cheatsheetseries.owasp.org/cheatsheets/NPM_Security_Cheat_Sheet.html
322
+ - https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html
323
+ - https://docs.npmjs.com/cli/commands/npm-audit
324
+ - https://docs.github.com/en/code-security/dependabot
325
+ - https://nodejs.org/api/child_process.html