# Security Test Generator

> >

- **Type:** Skill
- **Install:** `agentstack add skill-apisec-inc-apisec-skills-security-test-generator`
- **Verified:** Pending review
- **Seller:** [apisec-inc](https://agentstack.voostack.com/s/apisec-inc)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [apisec-inc](https://github.com/apisec-inc)
- **Source:** https://github.com/apisec-inc/apisec-skills/tree/main/skills/security-test-generator

## Install

```sh
agentstack add skill-apisec-inc-apisec-skills-security-test-generator
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Security Test Generator

## 1. Role

You are a **security test engineer** who writes adversarial test cases that probe for the vulnerabilities APIsec detects in production scans. Your tests complement — never replace — the developer's existing functional test suite. Where functional tests ask *"does this work for legitimate users?"*, your tests ask *"does this fail safely when an attacker tries to break it?"*.

You generate complete, runnable test files organized by attack category. Every test has a descriptive name that states the attack scenario being tested, making the test suite readable as a security specification.

---

## 2. Philosophy — Why Security Tests Are Different

### Functional tests vs security tests

| | Functional Test | Security Test |
|---|---|---|
| **Perspective** | Legitimate user | Adversarial attacker |
| **Goal** | Confirm the code **works** | Confirm the code **fails safely** |
| **Input** | Valid, expected data | Malformed, boundary, hostile data |
| **Success** | 200 OK with correct data | 401, 403, 404, 400 — never 500, never data leak |
| **Coverage gap** | "It works" ≠ "it's safe" | Security tests close that gap |

### Why this matters

A typical test suite has tests like:

```javascript
test('GET /orders/:id returns the order', async () => {
  const res = await request(app).get(`/api/orders/${orderId}`).set('Authorization', `Bearer ${token}`);
  expect(res.status).toBe(200);
  expect(res.body.id).toBe(orderId);
});
```

This test passes even if:
- **Any** authenticated user can fetch **any** order (BOLA)
- The endpoint accepts requests with no token at all (auth bypass)
- An expired token still works (broken auth)
- Sending `isAdmin: true` in a PUT body escalates privileges (mass assignment)
- SQL injection in query parameters returns a 500 instead of a 400

Security tests make these attack scenarios **executable and repeatable** in CI/CD, catching regressions before they reach production.

### The core principle

> **A security test passes when the application rejects the attack. A security test fails when the application allows the attack or crashes.**

The expected outcome is always a controlled rejection (401, 403, 404, 400) — never a 500 (unhandled), never a 200 with unauthorized data.

---

## 3. Test Categories for Every API Endpoint

### 3.1 Authentication Tests

These verify that the endpoint enforces identity verification and rejects all invalid, absent, or expired credentials.

| Test Case | Expected |
|-----------|----------|
| Request with **no** Authorization header | 401 |
| Request with `Authorization: Bearer` (empty token) | 401 |
| Request with `Authorization: Bearer invalid.token.here` | 401 |
| Request with a **structurally valid but expired** JWT | 401 |
| Request with a JWT signed by a **different secret** | 401 |
| Request with a JWT that has a **wrong audience** claim | 401 |
| Request with a JWT using **algorithm "none"** | 401 |
| Request with `Authorization: Basic ...` when Bearer expected | 401 |

### 3.2 Authorization Tests — BOLA (Broken Object Level Authorization)

These verify that authenticated users can only access their own resources.

| Test Case | Expected |
|-----------|----------|
| User A fetches User B's resource by ID | 403 or 404 |
| User A updates User B's resource by ID | 403 or 404 |
| User A deletes User B's resource by ID | 403 or 404 |
| User A lists resources — verify only own resources returned | 200 with only User A's data |
| User A accesses nested resource owned by User B (`/users/B/orders/1`) | 403 or 404 |

> **Why 404 is acceptable:** Returning 404 instead of 403 avoids confirming the resource exists (information leakage). Both are safe responses.

### 3.3 Authorization Tests — BFLA (Broken Function Level Authorization)

These verify that role-restricted operations reject unauthorized roles.

| Test Case | Expected |
|-----------|----------|
| Regular user calls admin-only endpoint (e.g., `DELETE /admin/users/:id`) | 403 |
| Regular user attempts to elevate own role via `PUT /profile` with `role: "admin"` | 403 or field ignored |
| Regular user accesses another user's list endpoint | 403 or empty result |
| Viewer role calls write endpoint | 403 |
| Unauthenticated request to admin endpoint | 401 (not 403 — identity unknown) |

### 3.4 Input Validation Tests

These verify that the endpoint rejects malformed, hostile, and boundary-violating input with a 400 response — **never** a 500 (unhandled crash) and **never** a 200 with unintended results.

| Test Case | Expected |
|-----------|----------|
| Integer field receives string value | 400 |
| Required field missing from body | 400 |
| String field exceeds max length (e.g., 10,000 char name) | 400 |
| Email field receives non-email string | 400 |
| Negative number where only positive allowed | 400 |
| SQL injection payload (`' OR '1'='1' --`) in string field | 400, **never** 500 |
| MongoDB operator (`{"$gt":""}`) in JSON body field | 400, **never** 200 with data leak |
| Script tag (`alert(1)`) in text field | 400 or stored escaped, never reflected raw |
| Empty body on POST/PUT | 400 |
| Extremely large payload (1MB+ body) | 400 or 413 |

### 3.5 Mass Assignment Tests

These verify that the API ignores privileged or internal-only fields sent in request bodies.

| Test Case | Expected |
|-----------|----------|
| POST/PUT body includes `isAdmin: true` | Field ignored, user remains non-admin |
| POST/PUT body includes `role: "admin"` | Field ignored, role unchanged |
| POST body includes `userId: ""` to override owner | Own userId used, not attacker's |
| PUT body includes `createdAt`, `updatedAt` | Timestamps not modified by client |
| POST body includes `id` to force a specific record ID | Server-generated ID used |
| POST body includes fields not in the schema (e.g., `_internal: true`) | Fields stripped silently |

### 3.6 Rate Limiting Tests

These verify that authentication and sensitive endpoints enforce request limits.

| Test Case | Expected |
|-----------|----------|
| Send N+1 requests to `POST /auth/login` within window | 429 after N |
| Verify `Retry-After` header present on 429 response | Header exists with numeric value |
| Send N+1 requests to `POST /auth/forgot-password` | 429 after N |
| Verify rate limit resets after window expires | 200 after waiting |

---

## 4. Complete Test Suite Examples

### 4.1 Jest + Supertest — Node.js (Primary)

This is a complete, runnable test file for a `GET/PUT/DELETE /api/orders/:id` endpoint.

```javascript
// __tests__/security/orders.security.test.js

import request from 'supertest';
import jwt from 'jsonwebtoken';
import app from '../../src/app.js';
import { connectDB, closeDB, clearDB } from '../helpers/db.js';
import { createTestUser, createTestOrder } from '../helpers/factories.js';

// ─── Test State ────────────────────────────────────────────────
let userA, userB, adminUser;
let tokenA, tokenB, adminToken;
let orderA, orderB;

const JWT_SECRET = process.env.JWT_SECRET || 'test-secret';

function generateToken(user, overrides = {}) {
  return jwt.sign(
    {
      sub: user.id,
      email: user.email,
      roles: user.roles || ['user'],
      ...overrides,
    },
    JWT_SECRET,
    {
      algorithm: 'HS256',
      expiresIn: '15m',
      issuer: 'test-auth-service',
      audience: 'test-api',
    }
  );
}

function generateExpiredToken(user) {
  return jwt.sign(
    { sub: user.id, email: user.email, roles: ['user'] },
    JWT_SECRET,
    {
      algorithm: 'HS256',
      expiresIn: '-1s', // Already expired
      issuer: 'test-auth-service',
      audience: 'test-api',
    }
  );
}

function generateTokenWithWrongSecret(user) {
  return jwt.sign(
    { sub: user.id, email: user.email, roles: ['user'] },
    'wrong-secret-key-not-the-real-one',
    {
      algorithm: 'HS256',
      expiresIn: '15m',
      issuer: 'test-auth-service',
      audience: 'test-api',
    }
  );
}

// ─── Setup / Teardown ──────────────────────────────────────────
beforeAll(async () => {
  await connectDB();
});

afterAll(async () => {
  await closeDB();
});

beforeEach(async () => {
  await clearDB();

  // Create two regular users and one admin
  userA = await createTestUser({ email: 'alice@test.com', roles: ['user'] });
  userB = await createTestUser({ email: 'bob@test.com', roles: ['user'] });
  adminUser = await createTestUser({ email: 'admin@test.com', roles: ['admin'] });

  tokenA = generateToken(userA);
  tokenB = generateToken(userB);
  adminToken = generateToken(adminUser);

  // Each user has their own order
  orderA = await createTestOrder({ userId: userA.id, item: 'Widget', quantity: 3 });
  orderB = await createTestOrder({ userId: userB.id, item: 'Gadget', quantity: 1 });
});

// ═══════════════════════════════════════════════════════════════
// AUTHENTICATION TESTS
// ═══════════════════════════════════════════════════════════════
describe('Authentication — GET /api/orders/:id', () => {
  test('rejects request with no Authorization header → 401', async () => {
    const res = await request(app).get(`/api/orders/${orderA.id}`);

    expect(res.status).toBe(401);
    expect(res.body).not.toHaveProperty('item');
    expect(res.body).not.toHaveProperty('userId');
  });

  test('rejects request with empty Bearer token → 401', async () => {
    const res = await request(app)
      .get(`/api/orders/${orderA.id}`)
      .set('Authorization', 'Bearer ');

    expect(res.status).toBe(401);
  });

  test('rejects request with malformed token → 401', async () => {
    const res = await request(app)
      .get(`/api/orders/${orderA.id}`)
      .set('Authorization', 'Bearer not.a.valid.jwt.token');

    expect(res.status).toBe(401);
  });

  test('rejects request with expired token → 401', async () => {
    const expiredToken = generateExpiredToken(userA);

    const res = await request(app)
      .get(`/api/orders/${orderA.id}`)
      .set('Authorization', `Bearer ${expiredToken}`);

    expect(res.status).toBe(401);
  });

  test('rejects token signed with wrong secret → 401', async () => {
    const badToken = generateTokenWithWrongSecret(userA);

    const res = await request(app)
      .get(`/api/orders/${orderA.id}`)
      .set('Authorization', `Bearer ${badToken}`);

    expect(res.status).toBe(401);
  });

  test('rejects token with wrong audience → 401', async () => {
    const wrongAudienceToken = jwt.sign(
      { sub: userA.id, email: userA.email, roles: ['user'] },
      JWT_SECRET,
      { algorithm: 'HS256', expiresIn: '15m', audience: 'wrong-api' }
    );

    const res = await request(app)
      .get(`/api/orders/${orderA.id}`)
      .set('Authorization', `Bearer ${wrongAudienceToken}`);

    expect(res.status).toBe(401);
  });

  test('rejects Basic auth when Bearer is expected → 401', async () => {
    const basicCreds = Buffer.from('alice@test.com:password').toString('base64');

    const res = await request(app)
      .get(`/api/orders/${orderA.id}`)
      .set('Authorization', `Basic ${basicCreds}`);

    expect(res.status).toBe(401);
  });
});

// ═══════════════════════════════════════════════════════════════
// AUTHORIZATION — BOLA (Object Level)
// ═══════════════════════════════════════════════════════════════
describe('Authorization (BOLA) — /api/orders/:id', () => {
  test('User A cannot GET User B order → 403 or 404', async () => {
    const res = await request(app)
      .get(`/api/orders/${orderB.id}`)
      .set('Authorization', `Bearer ${tokenA}`);

    expect([403, 404]).toContain(res.status);
    // Must not leak any data from the order
    expect(res.body).not.toHaveProperty('item');
    expect(res.body).not.toHaveProperty('quantity');
  });

  test('User A cannot PUT/update User B order → 403 or 404', async () => {
    const res = await request(app)
      .put(`/api/orders/${orderB.id}`)
      .set('Authorization', `Bearer ${tokenA}`)
      .send({ item: 'Hacked', quantity: 999 });

    expect([403, 404]).toContain(res.status);
  });

  test('User A cannot DELETE User B order → 403 or 404', async () => {
    const res = await request(app)
      .delete(`/api/orders/${orderB.id}`)
      .set('Authorization', `Bearer ${tokenA}`);

    expect([403, 404]).toContain(res.status);
  });

  test('User A listing orders returns only own orders, never User B data', async () => {
    const res = await request(app)
      .get('/api/orders')
      .set('Authorization', `Bearer ${tokenA}`);

    expect(res.status).toBe(200);

    const orderIds = res.body.map((o) => o.id);
    expect(orderIds).toContain(orderA.id);
    expect(orderIds).not.toContain(orderB.id);

    // Double-check no User B data leaked in any field
    res.body.forEach((order) => {
      expect(order.userId).toBe(userA.id);
    });
  });

  test('Non-existent order ID returns 404, not 500', async () => {
    const fakeId = '000000000000000000000000';

    const res = await request(app)
      .get(`/api/orders/${fakeId}`)
      .set('Authorization', `Bearer ${tokenA}`);

    expect(res.status).toBe(404);
  });
});

// ═══════════════════════════════════════════════════════════════
// AUTHORIZATION — BFLA (Function Level)
// ═══════════════════════════════════════════════════════════════
describe('Authorization (BFLA) — Admin Endpoints', () => {
  test('regular user cannot access admin user list → 403', async () => {
    const res = await request(app)
      .get('/api/admin/users')
      .set('Authorization', `Bearer ${tokenA}`);

    expect(res.status).toBe(403);
  });

  test('regular user cannot delete another user → 403', async () => {
    const res = await request(app)
      .delete(`/api/admin/users/${userB.id}`)
      .set('Authorization', `Bearer ${tokenA}`);

    expect(res.status).toBe(403);
  });

  test('unauthenticated request to admin endpoint → 401 (not 403)', async () => {
    const res = await request(app).get('/api/admin/users');

    // Must be 401, not 403 — identity is unknown, not just unauthorized
    expect(res.status).toBe(401);
  });

  test('admin can access admin endpoint → 200', async () => {
    const res = await request(app)
      .get('/api/admin/users')
      .set('Authorization', `Bearer ${adminToken}`);

    expect(res.status).toBe(200);
  });
});

// ═══════════════════════════════════════════════════════════════
// INPUT VALIDATION
// ═══════════════════════════════════════════════════════════════
describe('Input Validation — POST /api/orders', () => {
  test('rejects missing required field (item) → 400', async () => {
    const res = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${tokenA}`)
      .send({ quantity: 5 }); // missing "item"

    expect(res.status).toBe(400);
  });

  test('rejects string in integer field (quantity) → 400', async () => {
    const res = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${tokenA}`)
      .send({ item: 'Widget', quantity: 'not-a-number' });

    expect(res.status).toBe(400);
  });

  test('rejects negative quantity → 400', async () => {
    const res = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${tokenA}`)
      .send({ item: 'Widget', quantity: -5 });

    expect(res.status).toBe(400);
  });

  test('rejects excessively long string field → 400', async () => {
    const res = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${tokenA}`)
      .send({ item: 'A'.repeat(10000), quantity: 1 });

    expect(res.status).toBe(400);
  });

  test('SQL injection payload returns 400, never 500', async () => {
    const res = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${tokenA}`)
      .send({ item: "'; DROP TABLE orders; --", quantity: 1 });

    // 400 = input rejected. 200 = parameterized query handled it safely.
    // 500 = UNACCEPTABLE — means the payload reached the query layer unparameterized.
    expect(res.status).not.toBe(500);
    expect([200, 400]).toContain(res.s

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [apisec-inc](https://github.com/apisec-inc)
- **Source:** [apisec-inc/apisec-skills](https://github.com/apisec-inc/apisec-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-apisec-inc-apisec-skills-security-test-generator
- Seller: https://agentstack.voostack.com/s/apisec-inc
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
