From forge-kit-security
OWASP API Security Top 10 testing patterns, injection payloads, auth bypass vectors, and security test generation for REST APIs. Use when writing security tests, reviewing API endpoints for vulnerabilities, or auditing input validation.
How this skill is triggered — by the user, by Claude, or both
Slash command
/forge-kit-security:owasp-api-securityThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Comprehensive security testing knowledge base for REST APIs, aligned with OWASP API Security Top 10:2023 and OWASP ASVS 5.0.
Comprehensive security testing knowledge base for REST APIs, aligned with OWASP API Security Top 10:2023 and OWASP ASVS 5.0.
What: User A can access/modify User B's resources by manipulating IDs.
Test patterns:
// Access another user's resource
const res = await app.inject({
method: 'GET',
url: '/members/OTHER_USER_ID',
headers: { cookie: userASession },
})
expect(res.statusCode).toBe(403) // or 404 - never 200
// Modify another user's resource
const res = await app.inject({
method: 'PUT',
url: '/zones/OTHER_USER_ZONE_ID',
headers: { cookie: userASession },
payload: { name: 'hijacked' },
})
expect(res.statusCode).toBe(403)
Checklist:
:id params verified against session userTest patterns:
// No auth header
const res = await app.inject({ method: 'GET', url: '/members/me' })
expect(res.statusCode).toBe(401)
// Expired/invalid session
const res = await app.inject({
method: 'GET',
url: '/members/me',
headers: { cookie: 'better-auth.session_token=expired123' },
})
expect(res.statusCode).toBe(401)
// Malformed auth header
const res = await app.inject({
method: 'GET',
url: '/members/me',
headers: { authorization: 'Bearer <script>alert(1)</script>' },
})
expect(res.statusCode).toBe(401)
Test patterns:
const res = await app.inject({
method: 'PUT',
url: '/members/me',
headers: { cookie: session },
payload: { subscriptionTier: 'PREMIUM_PLUS', role: 'admin' },
})
// Verify tier/role unchanged in DB
Test patterns:
// Oversized payload
const res = await app.inject({
method: 'POST',
url: '/heartbeat',
headers: { cookie: session },
payload: { appVersion: 'x'.repeat(1_000_000) },
})
expect(res.statusCode).toBe(400) // or 413
Test patterns:
const res = await app.inject({
method: 'POST',
url: '/health-events',
headers: { cookie: freeUserSession },
payload: validHealthEvent,
})
expect(res.statusCode).toBe(403)
expect(res.json().code).toBe('SUBSCRIPTION_REQUIRED')
Test patterns:
// CORS: reject disallowed origins
// Stack traces: never exposed in error responses
const res = await app.inject({
method: 'POST',
url: '/nonexistent',
headers: { cookie: session },
})
expect(res.json()).not.toHaveProperty('stack')
expect(res.json()).toHaveProperty('error')
expect(res.json()).toHaveProperty('code')
// HTTP methods: reject unsupported methods
const res = await app.inject({ method: 'TRACE', url: '/heartbeat' })
expect([404, 405]).toContain(res.statusCode)
const sqlPayloads = [
"' OR '1'='1",
"'; DROP TABLE users; --",
"1; SELECT * FROM user --",
"' UNION SELECT null,null,null --",
"admin'--",
"1' AND 1=1 --",
"' OR 1=1 LIMIT 1 --",
]
const xssPayloads = [
'<script>alert(1)</script>',
'<img src=x onerror=alert(1)>',
'javascript:alert(1)',
'<svg onload=alert(1)>',
'"><script>alert(document.cookie)</script>',
"'-alert(1)-'",
'<iframe src="javascript:alert(1)">',
]
const cmdPayloads = [
'; ls -la',
'| cat /etc/passwd',
'$(whoami)',
'`id`',
'& ping -c 1 attacker.com',
'\n/bin/sh',
]
const nosqlPayloads = [
'{"$gt": ""}',
'{"$ne": null}',
'{"$where": "sleep(5000)"}',
]
const pathPayloads = [
'../../../etc/passwd',
'..\\..\\..\\windows\\system32',
'%2e%2e%2f%2e%2e%2f',
'....//....//....//etc/passwd',
]
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { buildApp } from '../../app'
import { createTestUser } from '../helpers/seed'
import type { FastifyInstance } from 'fastify'
describe('ENDPOINT security', () => {
let app: FastifyInstance
let user: TestUser
let otherUser: TestUser
beforeAll(async () => {
app = await buildApp({ enableEmailPassword: true })
user = await createTestUser(app)
otherUser = await createTestUser(app)
})
afterAll(async () => { await app.close() })
describe('authentication', () => {
it('rejects unauthenticated requests', async () => {
const res = await app.inject({ method: 'METHOD', url: '/path' })
expect(res.statusCode).toBe(401)
})
it('rejects invalid session tokens', async () => {
const res = await app.inject({
method: 'METHOD',
url: '/path',
headers: { cookie: 'better-auth.session_token=invalid' },
})
expect(res.statusCode).toBe(401)
})
})
describe('authorization (IDOR)', () => {
it('cannot access other users resources', async () => {
const res = await app.inject({
method: 'GET',
url: `/resource/${otherUser.resourceId}`,
headers: { cookie: user.sessionCookie },
})
expect([403, 404]).toContain(res.statusCode)
})
})
describe('input validation', () => {
it('rejects empty body', async () => {
const res = await app.inject({
method: 'POST',
url: '/path',
headers: { cookie: user.sessionCookie },
})
expect(res.statusCode).toBe(400)
})
it('rejects injection payloads', async () => {
for (const payload of sqlPayloads) {
const res = await app.inject({
method: 'POST',
url: '/path',
headers: { cookie: user.sessionCookie },
payload: { field: payload },
})
expect(res.statusCode).not.toBe(500)
}
})
})
describe('error response format', () => {
it('returns { error, code } without stack traces', async () => {
const res = await app.inject({
method: 'POST',
url: '/path',
headers: { cookie: user.sessionCookie },
payload: {},
})
if (res.statusCode >= 400) {
const body = res.json()
expect(body).toHaveProperty('error')
expect(body).toHaveProperty('code')
expect(body).not.toHaveProperty('stack')
expect(body).not.toHaveProperty('stackTrace')
}
})
})
})
Blocks Edit/Write/Bash actions until Claude investigates importers, data schemas, and user instructions. Improves output quality by forcing concrete facts before edits.
npx claudepluginhub agigante80/forge-kit --plugin forge-kit-security