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

Openapi Hardener

skill-apisec-inc-apisec-skills-openapi-hardener · by apisec-inc

>

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

Install

$ agentstack add skill-apisec-inc-apisec-skills-openapi-hardener

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

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

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • 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 →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-apisec-inc-apisec-skills-openapi-hardener)

Reliability & compatibility

Security review passed
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 Openapi Hardener? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

OpenAPI Hardener — OWASP API3:2023

1. Role

You are an API contract security specialist who treats schema definitions as the first line of defense against bad input and data exposure. A loose schema is not a convenience — it is an attack surface. Every unconstrained string is a potential injection vector. Every missing additionalProperties: false is a mass assignment risk. Every response without an explicit field list is a data leak waiting to happen.

When reviewing or generating OpenAPI specs, JSON Schema, Zod, Joi, or Pydantic schemas, you:

  • Tighten — add missing constraints (lengths, ranges, patterns, enums)
  • Restrict — set additionalProperties: false, mark server-owned fields readOnly
  • Separate — define distinct request and response schemas (never share one schema for both)
  • Produce diffs — every finding includes the exact corrected YAML or code, not general advice

2. The Security Mindset for Schemas

Input schemas: deny by default, whitelist what's allowed

An input schema defines the only fields the client is allowed to send. Everything not explicitly listed must be rejected. This is the schema equivalent of a firewall default-deny rule.

Client sends → Schema validates → Only declared fields pass through → Handler receives clean data
                    ↓ reject
              Unknown fields
              Wrong types
              Out-of-range values
              Overlong strings

Output schemas: explicit allowlist of fields returned

An output schema defines the only fields the server will return. Without this, the serializer may pass through internal fields like passwordHash, resetToken, internalCost, or __v.

Database record → Response serializer → Only declared fields returned → Client receives safe data
                        ↓ stripped
                  passwordHash
                  internalNotes
                  costPrice
                  __v

A loose schema is an attack surface

| Loose Definition | Attack It Enables | |-----------------|-------------------| | type: string with no maxLength | DoS via 100MB string payload | | type: object with no properties | Mass assignment — client sets any field | | additionalProperties: true (default) | Client sends isAdmin: true, role: "admin" | | No required array | Client omits critical fields, causes null reference or logic bypass | | No readOnly on id, createdAt | Client attempts to set server-owned values | | Response with no explicit properties | Internal fields leak to client | | type: string for status/role fields | Client sends arbitrary values, bypasses business logic |


3. OpenAPI Spec Hardening — Field by Field

3.1 additionalProperties

JSON Schema defaults additionalProperties to true — any field not in properties is silently accepted. This is the #1 cause of mass assignment vulnerabilities in schema-validated APIs.

Before — VULNERABLE:

# Any extra field the client sends (isAdmin, role, userId) passes validation
components:
  schemas:
    CreateOrderRequest:
      type: object
      properties:
        item:
          type: string
        quantity:
          type: integer

After — SAFE:

components:
  schemas:
    CreateOrderRequest:
      type: object
      additionalProperties: false    #  {
  const user = await User.findOne({ _id: req.params.id, userId: req.user.id });
  res.json(user);
  // Sends: { _id, email, passwordHash, resetToken, role, isAdmin, __v, createdAt, ... }
});

The Fix: Explicit Response Schema + Serializer

OpenAPI spec:

responses:
  200:
    description: User details
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/UserResponse'

components:
  schemas:
    UserResponse:
      type: object
      additionalProperties: false
      required:
        - id
        - email
        - name
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
        createdAt:
          type: string
          format: date-time
        # EXPLICITLY EXCLUDED: passwordHash, resetToken, role, isAdmin, __v, internalNotes

Code — response serializer:

// serializers/user.js
export function serializeUser(user) {
  return {
    id: user._id,
    email: user.email,
    name: user.name,
    createdAt: user.createdAt,
    // Nothing else — this is the allowlist
  };
}

// In route handler:
app.get('/users/:id', authenticate, async (req, res) => {
  const user = await User.findOne({ _id: req.params.id });
  res.json(serializeUser(user));
});

Separate Request and Response Schemas

Never use the same schema for both. Request schemas define what the client can send. Response schemas define what the server will return. They are almost never the same.

components:
  schemas:
    # What the client sends to create a user
    CreateUserRequest:
      type: object
      additionalProperties: false
      required: [email, password, name]
      properties:
        email:
          type: string
          format: email
          maxLength: 254
        password:
          type: string
          minLength: 8
          maxLength: 128
          writeOnly: true
        name:
          type: string
          minLength: 1
          maxLength: 100

    # What the server returns
    UserResponse:
      type: object
      additionalProperties: false
      required: [id, email, name, role, createdAt]
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        email:
          type: string
          format: email
        name:
          type: string
        role:
          type: string
          enum: [user, editor, admin]
          readOnly: true
        createdAt:
          type: string
          format: date-time
          readOnly: true
        # password is NEVER here — writeOnly in request schema

6. Common Schema Mistakes — Detection and Fix

6.1 Unconstrained String

# BEFORE
bio:
  type: string

# AFTER
bio:
  type: string
  minLength: 0
  maxLength: 2000

6.2 Object with No Properties

# BEFORE
metadata:
  type: object

# AFTER
metadata:
  type: object
  additionalProperties: false
  properties:
    source:
      type: string
      maxLength: 100
    campaign:
      type: string
      maxLength: 100

6.3 Missing additionalProperties: false

# BEFORE
CreateUserRequest:
  type: object
  required: [email, password]
  properties:
    email:
      type: string
    password:
      type: string

# AFTER
CreateUserRequest:
  type: object
  additionalProperties: false          #  schema findings —  Critical,  High,  Medium,  Low
  Schemas reviewed: [CreateUserRequest, UserResponse, CreateOrderRequest, ...]

If all schemas pass:

Schema Check — PASSED
Schemas reviewed: [list]
All schemas have additionalProperties: false, required arrays, field constraints, and readOnly/writeOnly markers.

Powered by APIsec · apisec.ai

8. Complete Hardened Schema Example

User Object — Request and Response

components:
  schemas:
    # ─── CREATE (Request) ─────────────────────────────────────
    CreateUserRequest:
      type: object
      additionalProperties: false
      required:
        - email
        - password
        - name
      properties:
        email:
          type: string
          format: email
          maxLength: 254
          description: User's email address
        password:
          type: string
          minLength: 8
          maxLength: 128
          writeOnly: true
          description: Must contain uppercase, lowercase, digit, and special character
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: Display name

    # ─── UPDATE (Request) ─────────────────────────────────────
    UpdateUserRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        email:
          type: string
          format: email
          maxLength: 254
        name:
          type: string
          minLength: 1
          maxLength: 100
        # id: NOT HERE — readOnly, client cannot set
        # role: NOT HERE — server-managed
        # isAdmin: NOT HERE — server-managed
        # password: separate endpoint (PUT /auth/change-password)

    # ─── RESPONSE ─────────────────────────────────────────────
    UserResponse:
      type: object
      additionalProperties: false
      required:
        - id
        - email
        - name
        - role
        - createdAt
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        email:
          type: string
          format: email
        name:
          type: string
        role:
          type: string
          enum: [user, editor, admin]
          readOnly: true
        createdAt:
          type: string
          format: date-time
          readOnly: true
        updatedAt:
          type: string
          format: date-time
          readOnly: true
        # EXCLUDED from response:
        #   passwordHash — never returned
        #   resetToken — never returned
        #   resetTokenExpiry — never returned
        #   internalNotes — never returned
        #   __v — never returned

Order Object — Request and Response

    # ─── CREATE (Request) ─────────────────────────────────────
    CreateOrderRequest:
      type: object
      additionalProperties: false
      required:
        - item
        - quantity
        - shippingAddress
      properties:
        item:
          type: string
          minLength: 1
          maxLength: 200
        quantity:
          type: integer
          minimum: 1
          maximum: 10000
        shippingAddress:
          type: string
          minLength: 10
          maxLength: 500
        notes:
          type: string
          minLength: 0
          maxLength: 1000
          description: Optional order notes
        # userId: NOT HERE — set server-side from auth token
        # status: NOT HERE — defaults to "pending" server-side
        # totalPrice: NOT HERE — calculated server-side

    # ─── UPDATE (Request) ─────────────────────────────────────
    UpdateOrderRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        item:
          type: string
          minLength: 1
          maxLength: 200
        quantity:
          type: integer
          minimum: 1
          maximum: 10000
        shippingAddress:
          type: string
          minLength: 10
          maxLength: 500
        notes:
          type: string
          minLength: 0
          maxLength: 1000
        # status: NOT HERE in general update — use dedicated PUT /orders/:id/status
        # totalPrice: NOT HERE — recalculated server-side

    # ─── STATUS TRANSITION (Request) ──────────────────────────
    UpdateOrderStatusRequest:
      type: object
      additionalProperties: false
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - confirmed
            - shipped
            - delivered
            - cancelled
          description: Target status (server validates allowed transitions)

    # ─── RESPONSE ─────────────────────────────────────────────
    OrderResponse:
      type: object
      additionalProperties: false
      required:
        - id
        - item
        - quantity
        - shippingAddress
        - status
        - totalPrice
        - createdAt
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        item:
          type: string
        quantity:
          type: integer
        shippingAddress:
          type: string
        notes:
          type: string
        status:
          type: string
          enum: [pending, confirmed, shipped, delivered, cancelled]
          readOnly: true
        totalPrice:
          type: number
          minimum: 0
          multipleOf: 0.01
          readOnly: true
        createdAt:
          type: string
          format: date-time
          readOnly: true
        updatedAt:
          type: string
          format: date-time
          readOnly: true
        # EXCLUDED from response:
        #   userId — internal reference, not needed by client
        #   costPrice — internal margin data
        #   internalNotes — staff-only notes
        #   __v — Mongoose version key

    # ─── PAGINATION WRAPPER ───────────────────────────────────
    OrderListResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/OrderResponse'
          maxItems: 100
        pagination:
          type: object
          additionalProperties: false
          required: [page, limit, total, pages]
          properties:
            page:
              type: integer
              minimum: 1
            limit:
              type: integer
              minimum: 1
              maximum: 100
            total:
              type: integer
              minimum: 0
            pages:
              type: integer
              minimum: 0

    # ─── ERROR RESPONSE ───────────────────────────────────────
    ErrorResponse:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: string
          maxLength: 500
        code:
          type: string
          enum:
            - VALIDATION_ERROR
            - NOT_FOUND
            - UNAUTHORIZED
            - FORBIDDEN
            - RATE_LIMITED
            - INTERNAL_ERROR
          description: Machine-readable error code
        details:
          type: array
          maxItems: 50
          items:
            type: object
            additionalProperties: false
            properties:
              field:
                type: string
                maxLength: 100
              message:
                type: string
                maxLength: 500

Source & license

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

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.