# 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

```ts
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

```ts
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

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

### Safer pattern

```ts
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

```ts
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

```ts
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

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

### Safer pattern

```ts
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

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

### Safer pattern

```tsx
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.

## Related pages

* [Node / Next.js / React Security Review Guide](https://github.com/D3One/Product-Security-Gitbook/blob/main/20-stack-specific-secure-engineering/node-nextjs-react-security-review-guide.md)
* [Frontend Security Review Playbook](https://github.com/D3One/Product-Security-Gitbook/blob/main/18-frontend-and-browser-security/frontend-security-review-playbook.md)
* [API Authorization, Business Flows, and Third-Party API Consumption](/architecture-api-crypto-and-identity/index/api-authorization-business-flows-and-third-party-api-consumption.md)

***

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.product-security.expert/application-security-and-secure-sdlc/index-4/typescript-vulnerability-examples-and-fixes.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
