by thetestingacademy
Comprehensive API security testing based on OWASP API Security Top 10 including broken authentication, injection attacks, rate limiting, BOLA/BFLA vulnerabilities, and automated security scanning with ZAP and custom scripts.
npx @qaskills/cli add api-security-testingAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
You are an expert in API security testing. When the user asks you to test API security, implement OWASP API Top 10 checks, detect authentication and authorization vulnerabilities, or set up automated security scanning, follow these detailed instructions.
security-tests/
owasp/
bola.test.ts
broken-auth.test.ts
broken-object-property.test.ts
unrestricted-resource.test.ts
broken-function-level-auth.test.ts
mass-assignment.test.ts
ssrf.test.ts
security-misconfiguration.test.ts
improper-inventory.test.ts
unsafe-api-consumption.test.ts
auth/
token-validation.test.ts
session-management.test.ts
credential-handling.test.ts
oauth-flow.test.ts
injection/
sql-injection.test.ts
nosql-injection.test.ts
command-injection.test.ts
xss-injection.test.ts
header-injection.test.ts
rate-limiting/
rate-limit.test.ts
resource-exhaustion.test.ts
helpers/
api-client.ts
token-generator.ts
payload-generator.ts
vulnerability-reporter.ts
config/
security-config.ts
endpoints.ts
reports/
.gitkeep
// security-tests/owasp/bola.test.ts
import { describe, it, expect, beforeAll } from 'vitest';
import { SecurityApiClient } from '../helpers/api-client';
describe('BOLA - Broken Object Level Authorization', () => {
let userAClient: SecurityApiClient;
let userBClient: SecurityApiClient;
let adminClient: SecurityApiClient;
let userAResourceId: string;
beforeAll(async () => {
userAClient = await SecurityApiClient.authenticateAs('userA');
userBClient = await SecurityApiClient.authenticateAs('userB');
adminClient = await SecurityApiClient.authenticateAs('admin');
// Create a resource owned by User A
const response = await userAClient.post('/api/resources', { name: 'Private Resource' });
userAResourceId = response.data.id;
});
it('should prevent User B from accessing User A resources', async () => {
const response = await userBClient.get(`/api/resources/${userAResourceId}`);
expect(response.status).toBe(403);
});
it('should prevent User B from modifying User A resources', async () => {
const response = await userBClient.put(`/api/resources/${userAResourceId}`, {
name: 'Hacked',
});
expect(response.status).toBe(403);
});
it('should prevent User B from deleting User A resources', async () => {
const response = await userBClient.delete(`/api/resources/${userAResourceId}`);
expect(response.status).toBe(403);
});
it('should prevent IDOR via numeric ID enumeration', async () => {
// Try accessing resources by incrementing/decrementing IDs
const numericId = parseInt(userAResourceId, 10);
if (!isNaN(numericId)) {
for (let offset = -5; offset <= 5; offset++) {
if (offset === 0) continue;
const testId = numericId + offset;
const response = await userBClient.get(`/api/resources/${testId}`);
expect([403, 404]).toContain(response.status);
}
}
});
it('should prevent IDOR via UUID guessing', async () => {
// Try variations of the UUID
const uuidVariations = [
userAResourceId.replace(/-/g, ''),
userAResourceId.toUpperCase(),
userAResourceId.slice(0, -1) + '0',
];
for (const variation of uuidVariations) {
const response = await userBClient.get(`/api/resources/${variation}`);
if (response.status === 200) {
expect.fail(`BOLA vulnerability: User B accessed resource with ID variation: ${variation}`);
}
}
});
it('should prevent accessing resources via nested endpoints', async () => {
// Test nested resource access patterns
const nestedEndpoints = [
`/api/users/${userAResourceId}/profile`,
`/api/resources/${userAResourceId}/details`,
`/api/resources/${userAResourceId}/comments`,
];
for (const endpoint of nestedEndpoints) {
const response = await userBClient.get(endpoint);
expect([403, 404]).toContain(response.status);
}
});
});
// security-tests/auth/token-validation.test.ts
import { describe, it, expect } from 'vitest';
import { SecurityApiClient } from '../helpers/api-client';
describe('Authentication - Token Validation', () => {
it('should reject requests without tokens', async () => {
const client = new SecurityApiClient();
const response = await client.get('/api/protected-resource');
expect(response.status).toBe(401);
});
it('should reject expired tokens', async () => {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDAwMDAwMDB9.expired';
const client = new SecurityApiClient(expiredToken);
const response = await client.get('/api/protected-resource');
expect(response.status).toBe(401);
});
it('should reject malformed tokens', async () => {
const malformedTokens = [
'not-a-jwt',
'Bearer invalid',
'eyJhbGciOiJub25lIn0.eyJ0ZXN0IjoiZGF0YSJ9.',
'',
'null',
'undefined',
];
for (const token of malformedTokens) {
const client = new SecurityApiClient(token);
const response = await client.get('/api/protected-resource');
expect(response.status).toBe(401);
}
});
it('should reject tokens with algorithm none attack', async () => {
// JWT with alg:none header
const noneAlgToken = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url')
+ '.' + Buffer.from(JSON.stringify({ sub: '1', role: 'admin' })).toString('base64url')
+ '.';
const client = new SecurityApiClient(noneAlgToken);
const response = await client.get('/api/admin/users');
expect(response.status).toBe(401);
});
it('should reject tokens signed with wrong key', async () => {
// This would be a JWT signed with an attacker-controlled key
const wrongKeyToken = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwicm9sZSI6ImFkbWluIn0.wrong_signature';
const client = new SecurityApiClient(wrongKeyToken);
const response = await client.get('/api/protected-resource');
expect(response.status).toBe(401);
});
it('should not expose token details in error responses', async () => {
const client = new SecurityApiClient('invalid-token');
const response = await client.get('/api/protected-resource');
const body = await response.text();
expect(body).not.toContain('invalid-token');
expect(body).not.toContain('secret');
expect(body).not.toContain('key');
});
});
// security-tests/injection/sql-injection.test.ts
import { describe, it, expect, beforeAll } from 'vitest';
import { SecurityApiClient } from '../helpers/api-client';
import { SQL_INJECTION_PAYLOADS } from '../helpers/payload-generator';
describe('SQL Injection Testing', () => {
let client: SecurityApiClient;
beforeAll(async () => {
client = await SecurityApiClient.authenticateAs('userA');
});
const sqlPayloads = [
"' OR '1'='1",
"'; DROP TABLE users; --",
"' UNION SELECT username, password FROM users --",
"1; SELECT * FROM information_schema.tables",
"' OR 1=1 --",
"admin'--",
"1' ORDER BY 1--",
"' AND 1=CONVERT(int, (SELECT TOP 1 table_name FROM information_schema.tables))--",
];
it('should reject SQL injection in query parameters', async () => {
for (const payload of sqlPayloads) {
const response = await client.get(`/api/users?search=${encodeURIComponent(payload)}`);
expect([400, 200]).toContain(response.status);
if (response.status === 200) {
const body = await response.json();
// Verify no data leak
expect(JSON.stringify(body)).not.toContain('information_schema');
expect(JSON.stringify(body)).not.toContain('password');
}
}
});
it('should reject SQL injection in path parameters', async () => {
for (const payload of sqlPayloads) {
const response = await client.get(`/api/users/${encodeURIComponent(payload)}`);
expect([400, 404]).toContain(response.status);
}
});
it('should reject SQL injection in request body', async () => {
for (const payload of sqlPayloads) {
const response = await client.post('/api/users/search', { query: payload });
expect(response.status).not.toBe(500);
}
});
it('should not expose SQL error details in responses', async () => {
const response = await client.get("/api/users?id=' OR 1=1");
const body = await response.text();
expect(body.toLowerCase()).not.toContain('sql');
expect(body.toLowerCase()).not.toContain('syntax error');
expect(body.toLowerCase()).not.toContain('mysql');
expect(body.toLowerCase()).not.toContain('postgresql');
expect(body.toLowerCase()).not.toContain('sqlite');
});
});
// security-tests/rate-limiting/rate-limit.test.ts
import { describe, it, expect, beforeAll } from 'vitest';
import { SecurityApiClient } from '../helpers/api-client';
describe('Rate Limiting', () => {
let client: SecurityApiClient;
beforeAll(async () => {
client = await SecurityApiClient.authenticateAs('userA');
});
it('should enforce rate limits on authentication endpoint', async () => {
const responses = [];
for (let i = 0; i < 20; i++) {
const response = await client.post('/api/auth/login', {
email: 'test@test.com',
password: 'wrong',
});
responses.push(response.status);
}
const rateLimited = responses.filter((s) => s === 429);
expect(rateLimited.length).toBeGreaterThan(0);
});
it('should include rate limit headers', async () => {
const response = await client.get('/api/users');
const headers = response.headers;
// At least one rate limiting header should be present
const hasRateHeaders =
headers.get('x-ratelimit-limit') ||
headers.get('x-ratelimit-remaining') ||
headers.get('retry-after') ||
headers.get('ratelimit-limit');
expect(hasRateHeaders).toBeTruthy();
});
it('should limit request body size', async () => {
const largePayload = { data: 'x'.repeat(10 * 1024 * 1024) }; // 10MB
const response = await client.post('/api/data', largePayload);
expect([413, 400]).toContain(response.status);
});
it('should limit pagination size', async () => {
const response = await client.get('/api/users?limit=100000');
const body = await response.json();
if (response.status === 200) {
expect(body.data?.length || body.length || 0).toBeLessThanOrEqual(100);
}
});
});
// security-tests/helpers/api-client.ts
export class SecurityApiClient {
private baseUrl: string;
private token: string;
constructor(token = '') {
this.baseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
this.token = token;
}
static async authenticateAs(role: string): Promise<SecurityApiClient> {
const credentials: Record<string, { email: string; password: string }> = {
userA: { email: 'usera@test.com', password: 'TestPass123!' },
userB: { email: 'userb@test.com', password: 'TestPass456!' },
admin: { email: 'admin@test.com', password: 'AdminPass789!' },
};
const cred = credentials[role];
if (!cred) throw new Error(`Unknown role: ${role}`);
const response = await fetch(`${new SecurityApiClient().baseUrl}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cred),
});
const data = await response.json();
return new SecurityApiClient(data.token);
}
async get(path: string): Promise<Response> {
return fetch(`${this.baseUrl}${path}`, {
headers: this.getHeaders(),
});
}
async post(path: string, body: any): Promise<Response> {
return fetch(`${this.baseUrl}${path}`, {
method: 'POST',
headers: { ...this.getHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}
async put(path: string, body: any): Promise<Response> {
return fetch(`${this.baseUrl}${path}`, {
method: 'PUT',
headers: { ...this.getHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}
async delete(path: string): Promise<Response> {
return fetch(`${this.baseUrl}${path}`, {
method: 'DELETE',
headers: this.getHeaders(),
});
}
private getHeaders(): Record<string, string> {
const headers: Record<string, string> = {};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
return headers;
}
}
// security-tests/owasp/mass-assignment.test.ts
import { describe, it, expect, beforeAll } from 'vitest';
import { SecurityApiClient } from '../helpers/api-client';
describe('Mass Assignment Vulnerability', () => {
let userClient: SecurityApiClient;
beforeAll(async () => {
userClient = await SecurityApiClient.authenticateAs('userA');
});
it('should not allow setting role via user update endpoint', async () => {
const response = await userClient.put('/api/users/me', {
name: 'Updated Name',
role: 'admin',
});
// Verify the role was not changed
const profile = await userClient.get('/api/users/me');
const data = await profile.json();
expect(data.role).not.toBe('admin');
});
it('should not allow setting isVerified via registration', async () => {
const response = await userClient.post('/api/users', {
name: 'New User',
email: 'new@test.com',
password: 'Password123!',
isVerified: true,
isAdmin: true,
});
if (response.status === 201) {
const data = await response.json();
expect(data.isVerified).not.toBe(true);
expect(data.isAdmin).not.toBe(true);
}
});
it('should not allow modifying internal fields', async () => {
const internalFields = [
{ createdAt: '2020-01-01T00:00:00Z' },
{ updatedAt: '2020-01-01T00:00:00Z' },
{ deletedAt: null },
{ passwordHash: 'malicious_hash' },
{ accountBalance: 999999 },
];
for (const field of internalFields) {
const response = await userClient.put('/api/users/me', {
name: 'Test',
...field,
});
const profile = await userClient.get('/api/users/me');
const data = await profile.json();
const fieldName = Object.keys(field)[0];
expect(data[fieldName]).not.toBe(Object.values(field)[0]);
}
});
});
// security-tests/headers/security-headers.test.ts
import { describe, it, expect } from 'vitest';
describe('Security Headers', () => {
const BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000';
it('should include CORS headers', async () => {
const response = await fetch(BASE_URL, {
method: 'OPTIONS',
headers: { Origin: 'https://evil-site.com' },
});
const allowOrigin = response.headers.get('access-control-allow-origin');
if (allowOrigin) {
expect(allowOrigin).not.toBe('*');
expect(allowOrigin).not.toBe('https://evil-site.com');
}
});
it('should include security headers', async () => {
const response = await fetch(BASE_URL);
// Content-Type options
const xContentType = response.headers.get('x-content-type-options');
expect(xContentType).toBe('nosniff');
// Frame options
const xFrame = response.headers.get('x-frame-options');
expect(['DENY', 'SAMEORIGIN']).toContain(xFrame);
// Strict transport security
if (BASE_URL.startsWith('https')) {
const hsts = response.headers.get('strict-transport-security');
expect(hsts).toBeTruthy();
}
});
it('should not expose server information', async () => {
const response = await fetch(BASE_URL);
const server = response.headers.get('server');
const poweredBy = response.headers.get('x-powered-by');
// Server header should not reveal specific version
if (server) {
expect(server).not.toMatch(/\d+\.\d+/);
}
// X-Powered-By should not be present
expect(poweredBy).toBeNull();
});
});
- name: Install QA Skills
run: npx @qaskills/cli add api-security-testing12 of 29 agents supported