Write automated SQL injection test payloads, verify Prisma parameterized queries block injection, detect injection with sqlmap, and test raw query safety.
SQL injection remains one of the most exploited vulnerabilities despite being fully preventable. It occurs when user-controlled data is concatenated into SQL queries, allowing attackers to modify query logic and extract data. Prisma's parameterized queries prevent injection by construction — but developers sometimes bypass Prisma with raw SQL, reintroducing the vulnerability.
This lesson covers both: verifying parameterized queries are safe, and detecting injection in any raw SQL paths.
SQL injection exploits the boundary between data and SQL code. Classic payloads:
String-based (auth bypass):
1-- Input: email = ''' OR '1'='1
2-- Query: SELECT * FROM users WHERE email = '' OR '1'='1'
3-- Effect: returns all usersUNION-based (schema discovery):
1-- Input: productId = 1 UNION SELECT 1,username,password,4 FROM users--
2-- Effect: returns user credentials in product responseModern ORMs like Prisma prevent these by passing values as parameters:
1// SAFE: Prisma parameterized query
2const user = await prisma.user.findFirst({
3 where: { email: userEmail }, // email is a parameter, never concatenated
4});
5// Executed as: SELECT * FROM users WHERE email = $1
6
7// UNSAFE: string concatenation
8const user = await prisma.$queryRawUnsafe(
9 `SELECT * FROM users WHERE email = '${userEmail}'` // INJECTION!
10);Test your API endpoints with injection payloads to verify they return 400/401/404, NOT 500 (which indicates the payload reached the database).
1// apps/api/src/modules/auth/auth.injection.spec.ts
2const SQL_INJECTION_PAYLOADS = [
3 "' OR '1'='1",
4 "' OR '1'='1'--",
5 "'; DROP TABLE users;--",
6 "1 OR 1=1",
7 "1; SELECT sleep(5)--",
8 "' UNION SELECT 1,2,3--",
9 "admin'--",
10];
11
12describe('SQL Injection Prevention', () => {
13 SQL_INJECTION_PAYLOADS.forEach((payload) => {
14 it(`rejects payload: ${payload.substring(0, 30)}`, async () => {
15 const res = await request(app.getHttpServer())
16 .post('/auth/login')
17 .send({ email: payload, password: 'anypass' });
18
19 // MUST NOT return 500 — 500 means DB processed malformed query
20 expect(res.status).not.toBe(500);
21 // Must be 400 (validation) or 401 (invalid credentials)
22 expect([400, 401]).toContain(res.status);
23 // Must not expose database error messages
24 expect(res.body.message).not.toMatch(/syntax error|SQL|query|column|table/i);
25 });
26 });
27});Prisma $queryRaw with tagged template literals is safe. $queryRawUnsafe with string interpolation is not.
1// SAFE: tagged template literal — values become parameters
2const results = await prisma.$queryRaw`
3 SELECT id, title FROM courses
4 WHERE category = ${category}
5 ORDER BY rating DESC LIMIT ${limit}
6`;
7
8// UNSAFE: string interpolation
9const results = await prisma.$queryRawUnsafe(
10 `SELECT * FROM courses WHERE category = '${category}'` // INJECTION!
11);
12
13// Test to verify parameterization:
14describe('Prisma injection resistance', () => {
15 it('injection payload in category returns empty results, not DB error', async () => {
16 const injectionPayload = "' UNION SELECT id,email,password_hash,4,5 FROM users--";
17 await expect(
18 courseService.findByCategory(injectionPayload)
19 ).resolves.toEqual([]); // empty — no courses with that category
20 // If this throws or returns user data, the query is NOT parameterized
21 });
22});Verify at DB level: Enable PostgreSQL logging and confirm parameters appear as $1, $2 — never inline:
1# In postgresql.conf:
2# log_min_duration_statement = 0
3# Then check logs: WHERE category = $1 (safe) vs WHERE category = 'typescript' (unsafe)sqlmap automatically detects and exploits SQL injection vulnerabilities. Run it against your staging environment.
1# Install sqlmap
2pip install sqlmap
3
4# Scan a GET parameter
5sqlmap -u "https://staging.api.example.com/courses?category=typescript" \
6 --level=3 --risk=2 --dbms=postgresql --batch \
7 --output-dir=reports/sqlmap/
8
9# Authenticated scan with Bearer token
10sqlmap -u "https://staging.api.example.com/search" \
11 --method=POST \
12 --data='{"q":"typescript"}' \
13 --headers="Content-Type: application/json" \
14 --headers="Authorization: Bearer YOUR_JWT_TOKEN" \
15 --level=3 --risk=2 --batch
16
17# Expected on a safe API:
18# [INFO] GET parameter 'category' does not appear to be injectable
19# [CRITICAL] all tested parameters do not appear to be injectable
20
21# If vulnerable:
22# [CRITICAL] parameter 'id' is injectable (SQLi type: boolean-based blind)sqlmap options:
--level: 1-5, higher = more tests (1=basic, 5=exhaustive)--risk: 1-3, higher = more intrusive (3 may update data)--batch: non-interactive--dbms: target PostgreSQL specificallyEven with Prisma, injection can occur in middleware that constructs queries directly.
1// DANGEROUS: search middleware building raw query
2app.use('/api/search', (req, res, next) => {
3 const query = `SELECT * FROM courses WHERE title ILIKE '%${req.query.q}%'`; // INJECTION
4 db.query(query).then(results => res.json(results));
5});
6
7// SAFE: parameterized with pg library
8app.use('/api/search', async (req, res, next) => {
9 const q = String(req.query.q || '').slice(0, 200); // length limit
10 const results = await db.query(
11 'SELECT * FROM courses WHERE title ILIKE $1',
12 [`%${q}%`] // % is safe in the value, not in the query structure
13 );
14 res.json(results.rows);
15});
16
17// Best practices:
18// 1. Never concatenate user data into SQL
19// 2. Validate all query parameters with Zod before constructing queries
20// 3. DB user should only have SELECT/INSERT/UPDATE/DELETE, never DROP/ALTER
21// 4. Never expose raw database errors to clientsFull Jest test suite testing SQL injection, NoSQL injection, and command injection resistance.
1// apps/api/src/modules/search/__tests__/injection.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3import { INestApplication, ValidationPipe } from '@nestjs/common';
4import * as request from 'supertest';
5import { SearchModule } from '../search.module';
6import { PrismaModule } from '../../prisma/prisma.module';
7
8const INJECTION_PAYLOADS = [
9 "' OR '1'='1",
10 "'; DROP TABLE courses;--",
11 "' UNION SELECT 1,email,password_hash,4 FROM users--",
12 "1 OR 1=1",
13 "1; SELECT pg_sleep(5)--",
14 "' AND 1=CAST((SELECT version()) AS INT)--",
15 "%27 OR %271%27=%271", // URL encoded
16];
17
18describe('Search API Injection Resistance', () => {
19 let app: INestApplication;
20
21 beforeAll(async () => {
22 const module: TestingModule = await Test.createTestingModule({
23 imports: [SearchModule, PrismaModule],
24 }).compile();
25 app = module.createNestApplication();
26 app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
27 await app.init();
28 });
29
30 afterAll(() => app.close());
31
32 INJECTION_PAYLOADS.forEach((payload) => {
33 it('rejects: ' + payload.substring(0, 40), async () => {
34 const res = await request(app.getHttpServer())
35 .get('/search')
36 .query({ q: payload, limit: 10, page: 1 });
37
38 expect(res.status).not.toBe(500);
39 expect([200, 400]).toContain(res.status);
40
41 if (res.status === 200) {
42 const body = JSON.stringify(res.body);
43 expect(body).not.toMatch(/password|password_hash|secret|token/i);
44 expect(body).not.toMatch(/syntax error|pg_|postgres/i);
45 }
46 });
47 });
48
49 it('rejects non-numeric limit parameter', async () => {
50 const res = await request(app.getHttpServer())
51 .get('/search')
52 .query({ q: 'react', limit: '999; DROP TABLE courses--' });
53 expect(res.status).toBe(400);
54 });
55
56 it('enforces maximum limit to prevent DoS', async () => {
57 const res = await request(app.getHttpServer())
58 .get('/search')
59 .query({ q: 'react', limit: 99999 });
60 expect(res.status).toBe(400);
61 });
62});This test suite covers SQL injection payloads (7 variants). The core assertion: the API must never return HTTP 500 — a 500 response to an injection payload means malformed SQL reached the database. The test also checks that 200 responses don't contain sensitive fields (password_hash, tokens) that would indicate a UNION injection succeeded. ValidationPipe tests verify that non-numeric limit values are rejected at the input layer before any query is constructed.
The next lesson covers XSS payload testing and Content Security Policy validation — testing reflected and stored XSS, and verifying DOMPurify sanitization in React applications.
Your progress is saved in this browser, on this device only.