PS Product SecurityKnowledge Base

๐ŸŸฆ TypeScript Vulnerability Examples and Fixes

Use this page when: reviewing modern typed services and frontend code. TypeScript improves developer correctness, but it does not remove authorization, rendering, filesystem, or outbound-request risk.

How to read these examples

  • Vulnerable snippet shows the unsafe habit.
  • Safer pattern shows the direction you want in production code.
  • Why it matters ties the defect to attacker value and business impact.
  • Review cue is phrased so it can become a pull-request comment or checklist item.

Example 1 โ€” Broken object-level authorization in typed Express route

Vulnerable snippet

app.get('/api/invoices/:id', async (req, res) => {
  const invoice = await prisma.invoice.findUnique({
    where: { id: Number(req.params.id) }
  });
  res.json(invoice);
});

Safer pattern

app.get('/api/invoices/:id', async (req, res) => {
  const userId = req.auth!.userId;
  const invoice = await prisma.invoice.findFirst({
    where: { id: Number(req.params.id), ownerUserId: userId }
  });
  if (!invoice) return res.sendStatus(404);
  res.json(invoice);
});

Why it matters

  • Types tell you id is a number; they do not tell you the current user is allowed to see that object.

Business impact

  • Cross-customer data exposure and severe trust failures in APIs that appear otherwise well-structured.

Review cue

  • Typed parameters do not replace authorization. Scope queries by owner, tenant, or explicit permission.

Example 2 โ€” SQL injection with raw query helpers

Vulnerable snippet

const email = String(req.query.email || '');
const users = await prisma.$queryRawUnsafe(
  `SELECT id, role FROM users WHERE email = '${email}'`
);

Safer pattern

const email = String(req.query.email || '');
const users = await prisma.$queryRaw`
  SELECT id, role FROM users WHERE email = ${email}
`;

Why it matters

  • Unsafe raw-query helpers still let untrusted input rewrite the SQL statement even in strongly typed codebases.

Business impact

  • Data leakage, auth bypass in lookup logic, destructive queries, and audit findings against otherwise modern stacks.

Review cue

  • Prefer ORM filters first; when raw SQL is necessary, use parameterized helpers only.

Example 3 โ€” SSRF in webhook or URL-preview feature

Vulnerable snippet

app.post('/preview', async (req, res) => {
  const html = await fetch(String(req.body.url)).then(r => r.text());
  res.send(html.slice(0, 500));
});

Safer pattern

const ALLOWED = new Set(['status.example.com', 'cdn.example.com']);
app.post('/preview', async (req, res) => {
  const target = new URL(String(req.body.url));
  if (target.protocol !== 'https:' || !ALLOWED.has(target.hostname)) {
    return res.sendStatus(403);
  }
  const html = await fetch(target.toString(), { redirect: 'error' }).then(r => r.text());
  res.send(html.slice(0, 500));
});

Why it matters

  • The server fetches with its own network access and trust, so a preview feature can become an internal pivot.

Business impact

  • Metadata credential theft, internal discovery, or abuse of services that were never meant to be internet-reachable.

Review cue

  • Model every user-influenced outbound request as an SSRF surface. Define exactly which destinations are allowed.

Example 4 โ€” Path traversal in file-serving helper

Vulnerable snippet

app.get('/files/:name', (req, res) => {
  res.sendFile(`/srv/files/${req.params.name}`);
});

Safer pattern

import path from 'node:path';
app.get('/files/:name', (req, res) => {
  const base = path.resolve('/srv/files');
  const target = path.resolve(base, req.params.name);
  if (!target.startsWith(base + path.sep)) {
    return res.sendStatus(400);
  }
  res.sendFile(target);
});

Why it matters

  • Template strings are still just string concatenation when they touch a filesystem path.

Business impact

  • Leakage of secrets, keys, config, and internal documents from the server or container.

Review cue

  • Treat file access as a security boundary: resolve, normalize, and verify the final path against a fixed base.

Example 5 โ€” Unsafe rich-text rendering in React / TypeScript

Vulnerable snippet

export function UserBio({ bio }: { bio: string }) {
  return <div dangerouslySetInnerHTML={{ __html: bio }} />;
}

Safer pattern

import DOMPurify from 'dompurify';
export function UserBio({ bio }: { bio: string }) {
  const clean = DOMPurify.sanitize(bio, { USE_PROFILES: { html: true } });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

Why it matters

  • The type system only proves bio is a string, not that it is safe HTML.

Business impact

  • Account takeover, privilege abuse in admin consoles, and trust collapse in user-generated-content features.

Review cue

  • Use plain text by default. When HTML is required, sanitize deliberately and constrain allowed content.

Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.