TypeScript Patterns to Prevent the Most Common Security Bugs (Checklist for Bounties)
securitylibrarieschecklist

TypeScript Patterns to Prevent the Most Common Security Bugs (Checklist for Bounties)

UUnknown
2026-02-28
10 min read
Advertisement

A focused TypeScript security checklist for 2026: runtime validation, zod/io-ts patterns, sanitization, strict CSP, and supply-chain hardening to reduce bounty surface.

Stop losing bounties (and sleep): a pragmatic TypeScript security checklist for 2026

If you ship TypeScript apps, you already know types catch a class of bugs early. But attackers exploit runtime gaps—malformed inputs, unsafe DOM usage, mis-typed third-party data, and fragile config—none of which TypeScript's static checks alone will prevent. This article gives a focused, actionable security checklist (with code patterns and libraries) that reduces your vulnerability surface and makes your project much harder to exploit — and easier to defend in bug-bounty programs.

The short elevator checklist (readers: keep this handy)

  • Validate all boundaries at runtime — use zod/io-ts/TypeBox for request, env, and 3rd-party data.
  • Prefer unknown over any and avoid unsafely asserting types.
  • Sanitize outputs — DOMPurify or equivalent before innerHTML / dangerouslySetInnerHTML.
  • Enforce a strict CSP + SRI for web assets and inline-scripts/styles.
  • Never build SQL/OS commands via string concatenation — use parameterized queries and typed input schemas.
  • Pin and audit dependencies (lockfiles, supply-chain scanners: Snyk, Dependabot, Sigstore).
  • Type third-party libraries via @types/DefinitelyTyped or local declarations; don't cast away safety.
  • Automate security checks in CI: typecheck, ESLint security rules, Semgrep/CodeQL, and runtime integration tests.

Why this matters in 2026

Bug-bounty programs like the high-profile prizes we saw in 2024–2025 (multiple game studios and SaaS platforms increased payouts) reward high-impact runtime vulnerabilities: unauthenticated RCE, account takeover, mass data leakage. Many of those root causes are preventable with a disciplined TypeScript + runtime-validation strategy. In 2026, attacks increasingly exploit weak boundaries and supply-chain issues rather than raw parser bugs — so your defensive investments should reflect that.

Pattern 1 — Validate at every boundary: inputs, env, and 3rd-party responses

TypeScript types are a compile-time contract. Any data crossing process/network boundaries must be verified at runtime. Adopt a single validation library broadly: zod for ergonomics, io-ts or TypeBox + AJV for JSON-Schema compatibility and performance.

Why runtime validation?

  • Prevents malformed data from causing logic errors or unsafe casts.
  • Stops attackers from passing crafted payloads that skip type assumptions.
  • Produces canonical error messages and fail-fast behavior useful in bounties.

zod example: validate an API payload

import { z } from 'zod'

const CreateUser = z.object({
  username: z.string().min(3).max(30),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
})

// Express middleware
function validateCreateUser(req, res, next) {
  const result = CreateUser.safeParse(req.body)
  if (!result.success) return res.status(400).json({ error: result.error.format() })
  req.body = result.data // now strongly typed at runtime
  next()
}

Use safeParse / decode patterns — never cast any to an interface. For io-ts, check the decode result and return human-friendly errors.

Pattern 2 — Treat environment variables as untrusted data

Secrets, URLs, feature flags, and config values come from outside your type system. Validate them at startup and abort early if something is missing or malformed. This reduces misconfiguration-based exploits often highlighted in bounty reports.

const Env = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.string().regex(/^[0-9]+$/).transform(Number).default('3000'),
  FEATURE_X: z.string().optional(),
})

const env = Env.parse(process.env)

Pattern 3 — Use branded/opaque types to prevent accidental misuse

Branded or opaque types prevent accidental mixing of strings that have special meaning (IDs, emails, SQL fragments). They make unsafe concatenation and injection more visible at compile time.

type UserId = string & { readonly __brand: 'UserId' }

function makeUserId(id: string): UserId {
  // validate first
  if (!/^[0-9a-f]{24}$/.test(id)) throw new Error('invalid id')
  return id as UserId
}

// prevents passing a plain string where a UserId is expected

Pattern 4 — Sanitize outputs, not just inputs

Input validation prevents a class of malformed data, but XSS and other output-related flaws are caused by unsafe rendering. Escape and sanitize at the point of rendering. For client-side DOM or SSR code that writes HTML, use well-maintained sanitizers.

Libraries and practical tips (2026)

  • DOMPurify remains the de-facto sanitizer for browser/SSR — canonical usage: sanitize before innerHTML or dangerouslySetInnerHTML.
  • For Node-based HTML fragments, prefer server-side variants or libraries like sanitize-html that support server contexts (keep dependencies minimal).
  • Always whitelist allowed tags and attributes instead of blacklisting.
import DOMPurify from 'dompurify'

function renderUserBio(bioHtml: string) {
  const safe = DOMPurify.sanitize(bioHtml)
  // React example
  return <div dangerouslySetInnerHTML={{ __html: safe }} />
}

Pattern 5 — Enforce a strict Content Security Policy + SRI

CSP is a runtime control that mitigates XSS even when sanitization fails. In 2026, CSP combined with Subresource Integrity (SRI) and secure headers is table-stakes for public web apps.

  • Set CSP to disallow inline scripts/styles; use nonces when necessary.
  • Use SRI for third-party scripts (CDNs) so injected script files are rejected if altered.
  • Deploy policies via server headers (not meta tags) and test with report-only before enforcing.

Pattern 6 — Avoid dangerous APIs and patterns

Some JavaScript/Node APIs are frequently abused. Ban or guard them:

  • Avoid eval, new Function, or dynamic code loaders unless absolutely necessary.
  • Never call child_process.exec with interpolated strings; use parameterized APIs and safe argument arrays.
  • For databases, always use parameterized queries or ORM prepared statements. No string-concatenated SQL.
  • Guard HTTP client requests when used to fetch arbitrary URLs — implement allowlists to prevent SSRF.
// BAD: vulnerable to command injection
child_process.exec(`convert ${userInput} out.png`)

// GOOD: use an API that accepts args safely
child_process.spawn('convert', [userInput, 'out.png'])

Pattern 7 — Type third-party data and lock down typings

Many vulnerabilities emerge from incorrectly typed third-party responses. Contribute to or pin @types/DefinitelyTyped when possible, but always validate at runtime. Use local declaration files when you need temporary typings, and remove casts.

  • Prefer --noImplicitAny and strict in tsconfig; keep skipLibCheck false where feasible.
  • If a library lacks types, add a minimal declaration (declares) in your repo instead of casting every import to any.
  • Use typed fetch wrappers that decode responses with zod/io-ts before returning typed results.
// typed fetch wrapper
async function fetchJson<T>(url: string, schema: z.ZodType<T>) {
  const r = await fetch(url)
  const json = await r.json()
  return schema.parse(json) // throws if shape differs
}

Pattern 8 — Prevent injection by using typed parameters and ORM helpers

SQL/NoSQL and shell injections are still common bounty winners. The pattern is consistent: never interpolate untrusted data into a query or command string.

// Using pg (node-postgres)
const res = await client.query('SELECT * FROM users WHERE id = $1', [userId])

// Using Prisma/ORM — also validate/sanitize before passing
const user = await prisma.user.findUnique({ where: { id: validatedId } })

Pattern 9 — Secure supply chain and typings

2024–2026 saw more sophisticated supply-chain attacks. Your TypeScript ecosystem introduces extra surfaces: DefinitelyTyped packages, build-time tools, and codegen. Harden them:

  • Pin versions and check lockfiles in CI.
  • Use reproducible builds and signed artifacts (Sigstore, cosign).
  • Run automated dependency scans and monitor alerts from Snyk/GitHub Dependabot.
  • Be cautious accepting new @types packages; inspect them — a malicious type package can run postinstall scripts if poorly configured.

Pattern 10 — Automate checks in CI and in PRs

Manual reviews miss things. Automate security and type hygiene:

  • Fail builds on type errors and lint warnings related to security (@typescript-eslint/no-explicit-any, no-non-null-assertion).
  • Run Semgrep patterns for common injection patterns and unsafe API usage.
  • Use CodeQL or Snyk for deeper vulnerability analysis and SCA alerts.
  • Include runtime contract tests that fuzz inputs at endpoints and assert validation rejects malformed payloads.

How following this helps in a bug-bounty triage

Bounty programs reward impact and reproducibility. When you apply strict runtime validation, sanitization, CSP, and typed third-party interfacing, you do two things:

  • You reduce the number of critical bugs an attacker can find (less surface area).
  • When a vulnerability is reported, consistent validation layers and logs make it easier to reproduce and patch — and many programs give credit for high-quality reports.
“Most critical bounties are exploit chains — break the first link (untrusted input) and you stop many of them.”

Quick reference — Security ESLint + TypeScript rules

  • Enable strict, noImplicitAny, and noUncheckedIndexedAccess in tsconfig.
  • ESLint plugins: @typescript-eslint, eslint-plugin-security, eslint-plugin-no-unsanitized (flag unsafe DOM assignments), and eslint-plugin-sonarjs.
  • Ban non-null assertions unless audited: @typescript-eslint/no-non-null-assertion.

Sample end-to-end pattern: validate → type → persist

// 1) validate request body with zod
const Payload = z.object({ name: z.string(), email: z.string().email(), amount: z.number() })

// 2) validate & narrow
app.post('/donate', (req, res) => {
  const parsed = Payload.safeParse(req.body)
  if (!parsed.success) return res.status(400).json({ error: 'invalid input' })

  const { name, email, amount } = parsed.data

  // 3) use parameterized DB driver
  await db.query('INSERT INTO donations(name,email,amount) VALUES($1,$2,$3)', [name, email, amount])

  res.sendStatus(201)
})

What to log (and not to log) for bounty triage

Good logging helps triage without leaking secrets. Log validation failures with request IDs and schema errors, but avoid logging raw sensitive inputs (passwords, tokens). Include reproducible mutation steps and environment details in your bounties.

  • Expect richer runtime type tooling: more projects will combine compile-time types with zero-cost runtime checks (TypeScript + schema compilers).
  • Supply-chain signing and reproducible builds will be mainstream; integrate Sigstore into your CI to prove provenance.
  • AI-generated code increases the need for strict validation and stricter linting — never trust generated snippets without validation layers.

Final checklist — actionable steps you can complete in a sprint

  1. Enable TypeScript strict mode and relevant ESLint security rules.
  2. Add runtime validation (zod/io-ts) to all API endpoints and crucial internal boundaries.
  3. Validate and fail-fast on environment variables at startup.
  4. Sanitize any HTML outputs with DOMPurify; audit any uses of innerHTML / dangerouslySetInnerHTML.
  5. Publish a strict CSP and deploy SRI for external scripts; test in report-only mode first.
  6. Pin dependencies, enable lockfile verification in CI, and run SCA tools every PR.
  7. Replace string-concatenated SQL and shell commands with parameterized APIs.
  8. Automate Semgrep/CodeQL scans and add runtime integration tests that exercise validation logic.

Closing — ship safe, get fewer bounties (but higher-quality reports)

TypeScript gives you a head start, but purposeful runtime patterns close the attacks that actually win bug bounties. Treat types as part of a broader safety contract: validate at boundaries, sanitize on output, tighten CSP and supply-chain controls, and automate checks in CI. Do this and you’ll reduce exploitable surface area — and when a bounty does arrive, you’ll be ready to triage and patch efficiently.

Want a one-page checklist for your repo or a starter template that wires zod, DOMPurify, CSP headers, and CI checks together? Grab the GitHub gist and CI templates linked from the footer, or open an issue with your architecture for tailored recommendations.

Call to action

Harden one endpoint this week: add zod validation, sanitize outputs, and run a Semgrep rule. Then file a PR and observe the reduced attack surface. For templates, CI workflows, and a curated list of vetted security libraries for TypeScript, visit our GitHub repo or subscribe to our weekly TypeScript security digest.

Advertisement

Related Topics

#security#libraries#checklist
U

Unknown

Contributor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

Advertisement
2026-02-28T04:39:23.624Z