๐ฆ 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
idis 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
biois 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
- Frontend Security Review Playbook
- API Authorization, Business Flows, and Third-Party API Consumption
Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.