level 11 / injection-and-auth
Injection & Auth Vulnerabilities
Test for SQL injection, command injection, and authentication bypass vulnerabilities.
SQL Injection
SQL injection inserts malicious SQL into input fields to manipulate database queries.
Vulnerable query:
SELECT * FROM users WHERE email = '$email' AND password = '$pass'
Attack input: email = ' OR '1'='1' --
Result: SELECT * FROM users WHERE email = '' OR '1'='1' --' AND password = '...'
→ Returns all users, bypassing authentication.
Testing for SQLi with Playwright
const SQL_PAYLOADS = [
"' OR '1'='1",
"'; DROP TABLE users; --",
"' OR 1=1 --",
"\" OR \"\"=\"",
"' UNION SELECT null, username, password FROM users --",
];
test('login form rejects SQL injection payloads', async ({ page }) => {
for (const payload of SQL_PAYLOADS) {
await page.goto('/login');
await page.fill('[name=email]', payload);
await page.fill('[name=password]', payload);
await page.click('[type=submit]');
// Should NOT be logged in
await expect(page).not.toHaveURL(/dashboard/);
// Should show error message, not a DB error
const bodyText = await page.locator('body').textContent();
expect(bodyText?.toLowerCase()).not.toContain('sql');
expect(bodyText?.toLowerCase()).not.toContain('syntax error');
expect(bodyText?.toLowerCase()).not.toContain('unclosed quotation');
}
});
API SQLi Testing
test('search API rejects injection in query parameters', async ({ request }) => {
const payloads = ["' OR 1=1 --", "'; DROP TABLE products; --"];
for (const payload of payloads) {
const res = await request.get('/api/products', {
params: { search: payload },
});
// Should return 400 (validation error) or 200 with empty results
// Should NOT return 500 (DB error) or full table dump
expect(res.status()).not.toBe(500);
if (res.status() === 200) {
const body = await res.json();
// Should not return suspiciously large number of results
expect(body.length).toBeLessThan(100);
}
}
});
Error Disclosure Detection
SQLi often reveals itself through error messages. Test that errors are generic:
test('database errors are not exposed to users', async ({ request }) => {
// Send malformed data that might trigger DB errors
const res = await request.post('/api/users', {
data: { id: "' OR 1=1 --", name: null },
});
if (res.status() >= 400) {
const body = await res.text();
// DB error strings must not appear in response
const dbErrorPatterns = [
'syntax error', 'sql', 'postgresql', 'mysql', 'sqlite',
'ORA-', 'ODBC', 'unclosed quotation', 'unterminated string',
];
for (const pattern of dbErrorPatterns) {
expect(body.toLowerCase()).not.toContain(pattern);
}
}
});
Authentication Bypass
test('cannot log in with empty credentials', async ({ request }) => {
const res = await request.post('/api/login', {
data: { username: '', password: '' },
});
expect([400, 401]).toContain(res.status());
});
test('cannot access protected route without token', async ({ request }) => {
const res = await request.get('/api/protected');
expect(res.status()).toBe(401);
});
test('expired token is rejected', async ({ request }) => {
// An old/invalid JWT
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxfQ.invalid';
const res = await request.get('/api/profile', {
headers: { Authorization: `Bearer ${expiredToken}` },
});
expect(res.status()).toBe(401);
});
test('token from user A rejected for user B resource', async ({ request }) => {
const tokenA = await loginAs(request, 'user-a@test.com');
const userBId = 'user-b-uuid';
const res = await request.get(`/api/users/${userBId}/settings`, {
headers: { Authorization: `Bearer ${tokenA}` },
});
expect(res.status()).toBe(403);
});
Command Injection (Node.js)
const CMD_PAYLOADS = [
'; ls -la',
'| cat /etc/passwd',
'`whoami`',
'$(id)',
'& dir',
];
test('filename input rejects command injection', async ({ request }) => {
for (const payload of CMD_PAYLOADS) {
const res = await request.post('/api/files/convert', {
data: { filename: `document${payload}.pdf` },
});
// Should reject or sanitise — not execute OS command
expect([400, 422]).toContain(res.status());
if (res.status() === 200) {
const body = await res.text();
// Should not contain OS command output
expect(body).not.toContain('root:'); // /etc/passwd
expect(body).not.toContain('uid='); // id command output
}
}
});
Parameterised Queries — The Fix
Always verify backend uses parameterised queries — test engineer’s job is to confirm the code review standard:
// ✅ Safe — parameterised
await db.query('SELECT * FROM users WHERE email = $1', [email]);
// ❌ Unsafe — string interpolation (never write this)
await db.query(`SELECT * FROM users WHERE email = '${email}'`);