AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL unreviewed MIT Self-run

Security Test Generator

skill-apisec-inc-apisec-skills-security-test-generator · by apisec-inc

>

No reviews yet
0 installs
30 views
0.0% view→install

Install

$ agentstack add skill-apisec-inc-apisec-skills-security-test-generator

Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.

Security review

⚠ Flagged

1 finding(s); flagged for manual review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures
  • high Destructive filesystem operation.

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Reliability & compatibility

Not yet reviewed
0 installs to date
no reviews yet
6mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Security Test Generator? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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:

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.

// __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.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.