Secure Coding Practices for JavaScript and TypeScript Applications | SoniNow Blog

Limited TimeLearn More

secure codingjavascripttypescriptbest practicessecurity

Secure Coding Practices for JavaScript and TypeScript Applications

Published

2026-06-23

Read Time

6 mins

Secure Coding Practices for JavaScript and TypeScript Applications

JavaScript and TypeScript power the majority of modern web applications, from frontend interfaces to backend API servers. The dynamic nature of the language creates unique security challenges — prototype pollution, eval-based code injection, and supply chain risks that static languages handle differently.

Eliminating Dynamic Code Execution

The eval() function and its relatives — Function(), setTimeout(string), setInterval(string) — execute arbitrary strings as code. They are almost never justified in production applications.

// DANGEROUS — never use eval or Function constructor
function calculateExpression(input: string): number {
  // eslint-disable-next-line no-eval
  return eval(input); // "1; process.env.SECRET" — game over
}

// SAFE — use a proper expression parser
import { parse, evaluate } from 'expression-eval';

function safeCalculate(expression: string, context: Record<string, number>): number {
  const ast = parse(expression);
  return evaluate(ast, context);
}

// Even safer — restrict operations
function safeOperation(a: number, operator: string, b: number): number {
  switch (operator) {
    case '+': return a + b;
    case '-': return a - b;
    case '*': return a * b;
    case '/':
      if (b === 0) throw new Error('Division by zero');
      return a / b;
    default: throw new Error('Unknown operator');
  }
}

The same principle applies to new Function() and template-based code generation. If you find yourself building code as strings, there is almost always a safer architectural approach using configuration, strategy patterns, or expression parsers.

Prototype Pollution Prevention

Prototype pollution occurs when user-controlled data modifies Object.prototype, affecting all objects in the application. This can lead to property injection, privilege escalation, and denial of service.

// DANGEROUS — vulnerable to prototype pollution
function merge(target: Record<string, unknown>, source: Record<string, unknown>) {
  for (const key of Object.keys(source)) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key]) target[key] = {};
      merge(target[key] as Record<string, unknown>, source[key] as Record<string, unknown>);
    } else {
      target[key] = source[key];
    }
  }
}

// The attack payload:
// JSON.parse('{"__proto__": {"isAdmin": true}}')

// SAFE — prevent prototype keys explicitly
function safeMerge(target: Record<string, unknown>, source: Record<string, unknown>) {
  for (const key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue; // Skip dangerous keys
    }
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key] || typeof target[key] !== 'object') {
        target[key] = {};
      }
      safeMerge(target[key] as Record<string, unknown>, source[key] as Record<string, unknown>);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// Best: Use a library with built-in protection
import { merge } from 'lodash';
// Lodash fixed prototype pollution in v4.17.11+

Use Object.create(null) for hash maps to avoid prototype inheritance entirely:

// No prototype — immune to pollution
const safeMap = Object.create(null);
safeMap['__proto__'] = 'harmless'; // Just a property, not a prototype modification

Regular Expression Denial of Service (ReDoS)

User-supplied input used in regular expressions can cause catastrophic backtracking, freezing the event loop and creating a denial-of-service condition.

// DANGEROUS — ReDoS vulnerable regex
const emailRegex = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
// Input like "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aa" causes backtracking

// SAFE — use a validated regex without nested quantifiers
const safeEmailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

// SAFEST — use a parsing library for complex patterns
import { validate } from 'email-validator';
const isValid = validate(email);

Use re2 for user-supplied regex patterns — it rejects patterns with exponential backtracking:

import RE2 from 're2';

function safeMatch(pattern: string, input: string): boolean {
  try {
    const regex = new RE2(pattern); // Throws if pattern is ReDoS-vulnerable
    return regex.test(input);
  } catch {
    return false; // Pattern is unsafe, reject
  }
}

Output Encoding and Contextual Escaping

When rendering user-controlled data in different contexts, apply the correct encoding for each context. The wrong encoding — or no encoding — leads to XSS.

// HTML context — escape HTML entities
function escapeHtml(input: string): string {
  return input
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

// JavaScript string context — escape for string literals
function escapeJsString(input: string): string {
  return input
    .replace(/\\/g, '\\\\')
    .replace(/'/g, "\\'")
    .replace(/"/g, '\\"')
    .replace(/\n/g, '\\n')
    .replace(/\r/g, '\\r');
}

// URL context — encode for query parameters
function escapeUrl(input: string): string {
  return encodeURIComponent(input);
}

// CSS context — restrict to safe characters
function escapeCss(input: string): string {
  return input.replace(/[^a-zA-Z0-9\s-]/g, '');
}

In practice, modern frameworks handle context-aware escaping automatically. React escapes JSX content. Vue escapes template expressions. Angular has built-in sanitization. The risk comes from bypassing these protections with dangerouslySetInnerHTML, v-html, or raw string concatenation.

Secure Random Number Generation

JavaScript's Math.random() is not cryptographically secure. Never use it for security-sensitive operations — session IDs, CSRF tokens, API keys, password reset tokens, or encryption keys.

// INSECURE — Math.random() is predictable
const insecureToken = Math.random().toString(36).slice(2);

// SECURE — crypto.randomBytes or crypto.randomUUID
import crypto from 'crypto';

function generateSecureToken(bytes: number = 32): string {
  return crypto.randomBytes(bytes).toString('hex');
}

// Node 19+ and modern browsers: crypto.randomUUID
const uuid = crypto.randomUUID();

// For browser environments
function generateClientToken(): string {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('');
}

Type Safety as a Security Control

TypeScript's type system catches entire categories of security bugs at compile time. Strict mode prevents implicit any, null pointer dereferences, and type confusion.

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}
// Without strict: accidental undefined access
function processOrder(order: Order) {
  return order.total * 1.1; // Total might be undefined
}

// With strict: compiler catches the error
function processOrderSafe(order: Order): number {
  if (order.total === undefined) {
    throw new Error('Order total is required');
  }
  return order.total * 1.1;
}

Use branded types and discriminated unions to enforce domain constraints at the type level:

// Branded type — prevents ID confusion
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

function getUser(id: UserId): User { /* ... */ }
function getOrder(id: OrderId): Order { /* ... */ }

// TypeScript prevents mixing them up
getUser(orderId); // Type error!

Code Review Security Checklist

Incorporate security-specific checks into your code review process:

  • Does the code accept user input? Where is it validated?
  • Are SQL queries using parameterized statements?
  • Is there any eval(), Function(), or string-to-code conversion?
  • Are mutating merge/clone operations protected against prototype pollution?
  • Are file paths constructed from user input? Is path traversal prevented?
  • Are authentication checks present on every protected endpoint?
  • Are error messages generic (avoid leaking implementation details)?
  • Are secrets, tokens, or credentials appearing in logs or responses?
  • Is Math.random() used anywhere it shouldn't be?
  • Are dependencies current and vulnerability-free?

Secure coding in JavaScript and TypeScript requires awareness of the language's unique pitfalls and disciplined use of the type system. Our <a href="/services/web-development">web development team</a> embeds security practices into every code review and development workflow. Contact SoniNow to strengthen your application's code quality and security posture.