# Openapi Hardener

> >

- **Type:** Skill
- **Install:** `agentstack add skill-apisec-inc-apisec-skills-openapi-hardener`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **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/openapi-hardener

## Install

```sh
agentstack add skill-apisec-inc-apisec-skills-openapi-hardener
```

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

## 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:**

```yaml
# 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:**

```yaml
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:**

```yaml
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:**

```javascript
// 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.

```yaml
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

```yaml
# BEFORE
bio:
  type: string

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

### 6.2 Object with No Properties

```yaml
# 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

```yaml
# 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

```yaml
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

```yaml
    # ─── 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.

- **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:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **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: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-apisec-inc-apisec-skills-openapi-hardener
- 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%.
