An OpenAPI description that says nothing true about authentication, limits or audience lets insecure endpoints ship without anyone noticing. The OpenAPI Specification (OAS) is the machine-readable contract behind most REST APIs, and the same file now drives gateway configuration, generated clients, documentation portals, fuzzers and API inventories. That makes it a security control in its own right: if the contract is accurate and linted, missing authentication and unbounded inputs are caught in a pull request; if it is stale or hand-waved, every downstream tool inherits the gap. This guide covers the OAS security model (Security Scheme and Security Requirement Objects in 3.1 and 3.2), linting with Spectral and the OWASP ruleset, contract and schema-based testing, protecting the description itself, and how to roll this out across many teams. It is written for API platform owners, AppSec engineers and developers who own OpenAPI files. For the wider programme, see the API Security Guide.
Key takeaways
- Security declarations are claims, not enforcement. The
securityfield documents what the API expects; only the gateway or application enforces it. Contract tests must prove the two agree. - Empty arrays and empty objects mean anonymous. An operation with
security: []removes the top-level requirement, and{}in the list makes authentication optional. Both should fail CI unless explicitly allowlisted. - Lint every description in the pipeline. Spectral's built-in
oasruleset plus the OWASP API Security ruleset catch unprotected write operations, API keys in URLs, plain HTTP servers and unbounded strings before review. - Bounded schemas are a security feature.
maxLength,maxItems, integerformatandmaximum, andadditionalProperties: falseturn the contract into a validation policy the gateway can apply. - The description is sensitive. It maps every endpoint for an attacker. Publish internal descriptions only to authorised consumers, and treat third-party descriptions as untrusted input to your tooling.
What the OpenAPI security model can and cannot express
OAS describes security in two parts. A Security Scheme Object under components.securitySchemes defines how a caller authenticates. A Security Requirement Object, placed in the top-level security field or on an individual operation, says which schemes (and for OAuth 2.0 and OpenID Connect, which scopes) a request must satisfy. Version 3.2.0 is the current release of the specification; 3.1.x remains widely deployed, and the security objects are largely the same across both, with 3.2 adding the fields marked below.
Scheme type | Key fields | Security notes |
|---|---|---|
apiKey | name, in (query, header or cookie) | Never in: query: keys end up in proxy logs, browser history and analytics. The OWASP REST Security Cheat Sheet advises not relying on API keys alone for sensitive or high-value resources. |
http | scheme (IANA registered, case-insensitive), bearerFormat | bearerFormat: JWT is a documentation hint only. basic is supported for compatibility; the specification states inclusion is not an endorsement. |
mutualTLS | description | Added in 3.1. Describe the issuing CA and expected certificate attributes in the description, because there are no structured fields for them. |
oauth2 | flows (authorizationCode, clientCredentials, deviceAuthorization in 3.2, implicit, password), oauth2MetadataUrl in 3.2 | The specification points to the OAuth 2.0 Security BCP and recommends authorization code with PKCE. Do not declare implicit or password for new APIs. |
openIdConnect | openIdConnectUrl (discovery document) | Scopes in the requirement list are checked by the resource server, not by the description. |
Three semantics cause most real-world mistakes:
- Alternatives versus combinations: entries in the
securityarray are alternatives (any one satisfies the request). Schemes listed inside one Security Requirement Object must all be satisfied. Writing[{apiKey: []}, {oauth: [write]}]when you meant both is a silent downgrade. - Overrides: operation-level
securityreplaces the top-level value entirely. An empty array removes it; an empty object{}in the list declares anonymous access as an option. - Deprecation: 3.2 adds
deprecated: trueon a Security Scheme Object, which lets you signal a scheme migration (for example API key to OAuth) in the contract itself.
The specification's own security considerations add two points for tool builders: resolution of a Security Requirement to a scheme in multi-document descriptions is ambiguous in a way that can be abused (a component name identical to a URI wins over URI resolution), and descriptions can reference external resources that tools dereference automatically. Keep security schemes in the entry document and disable remote $ref resolution in CI unless the target is pinned and trusted.
Designing a secure contract
A contract that the gateway can enforce needs more than authentication. Treat these as the minimum for every operation:
- Authentication on every write: every
POST,PUT,PATCHandDELETEhas a non-empty Security Requirement. Public reads are listed by exception, with anx-extension recording who approved them. - Scopes that match functions: separate read and write scopes, and give administrative operations a different scheme or scope from user operations, so broken function-level authorisation (API5:2023) is visible in the contract.
- Unguessable identifiers: path parameters such as
{orderId}typed asstringwithformat: uuidrather than sequential integers. This does not replace object-level checks (API1:2023), it just removes the easy enumeration. - Bounded inputs: strings with
maxLength,enumorpattern; arrays withmaxItems; integers withformat(int32,int64) andminimum/maximum. These are the inputs to API4:2023 resource consumption controls. - Closed objects for writes: request bodies with
additionalProperties: falseand separate read and write schemas (orreadOnly/writeOnly) so mass assignment and excessive data exposure (API3:2023) are blocked by schema validation. - Documented errors and limits:
401,403,429and500responses with a fixed error schema (RFC 9457 problem details is a common choice), plus rate limit andRetry-Afterheaders on429. - Explicit audience: every
serversentry useshttps://and declares whether it is internal, so inventory tooling can separate partner, public and internal surfaces (API9:2023).
OWASP ASVS 5.0 backs this up from the verification side: V4.1.4 requires that only explicitly supported HTTP methods are accepted, V8.1.1 and V8.1.2 require documented function-level and field-level authorisation rules, and V13.4.5 requires that internal API documentation and monitoring endpoints are not exposed unless intended. A well-annotated OpenAPI file is the most practical place to keep that documentation. For token design behind the bearer scheme, see the JWT Security Guide.
Linting OpenAPI with Spectral and the OWASP ruleset
Spectral is an open source JSON and YAML linter with a built-in oas ruleset. Stoplight also publishes @stoplight/spectral-owasp-ruleset, whose v2.x is based on the OWASP API Security Top 10 2023 (v1.x targeted the 2019 edition). A project extends it with a one-line .spectral.yaml (extends: ["@stoplight/spectral-owasp-ruleset"]) and runs spectral lint against the description. The ruleset's own documentation is clear about the limit: it reads only the described surface, not the code behind it.
| Rule | What it flags | OWASP API risk |
|---|---|---|
oas3-operation-security-defined (built-in) | Operation security names a scheme not defined in components.securitySchemes | API2 Broken Authentication |
no-script-tags-in-markdown, no-eval-in-markdown (built-in) | Script or eval( in descriptions that may be rendered into HTML documentation | Tooling risk (XSS in portals) |
owasp:api2:2023-write-restricted | Write operations with no security scheme | API2 |
owasp:api2:2023-read-restricted | Read operations with no security scheme (information severity) | API2 |
owasp:api2:2023-no-api-keys-in-url | apiKey schemes placed in the query or path | API2 |
owasp:api2:2023-auth-insecure-schemes, owasp:api2:2023-no-http-basic | Outdated HTTP schemes such as NTLM or OAuth 1, and HTTP Basic | API2 |
owasp:api2:2023-short-lived-access-tokens | OAuth flows (other than client credentials) with no refreshUrl, implying long-lived access tokens | API2 |
owasp:api1:2023-no-numeric-ids | ID parameters typed as integers | API1 BOLA |
owasp:api4:2023-string-limit, array-limit, integer-limit | Unbounded strings, arrays and integers | API4 Unrestricted Resource Consumption |
owasp:api4:2023-rate-limit, rate-limit-responses-429 | 2XX and 4XX responses with no rate limit headers; missing 429 | API4 |
owasp:api5:2023-admin-security-unique | Admin paths sharing the same security scheme as normal paths | API5 BFLA |
owasp:api8:2023-no-server-http, define-cors-origin, define-error-responses-401 | Plain HTTP servers, missing CORS declaration, undefined 401 and 500 responses | API8 Security Misconfiguration |
owasp:api9:2023-inventory-access, inventory-environment | Servers without an x-internal audience flag or environment label | API9 Improper Inventory Management |
Rule IDs use the ruleset's own naming, which in places differs from the Top 10 numbering (the inventory rule is reported as api8 in some example output and defined as api9 in the source). Pin the ruleset version, and map findings by the rule ID your pinned version emits. Use overrides in your local ruleset to downgrade a rule for a specific path with a recorded reason, rather than disabling it globally.
Contract testing: proving the description matches the running API
Linting checks the document; it cannot see whether DELETE /accounts/{id} actually rejects an unauthenticated call. Schema-driven testing closes that gap by generating requests from the description and comparing responses with it.
| Technique | What it proves | Example tooling | Where to run |
|---|---|---|---|
| Negative authentication tests | Every operation with a Security Requirement returns 401 without credentials and 403 with insufficient scope | Generated test suite from the description, Schemathesis per-scheme auth | CI against an ephemeral environment |
| Response conformance | Responses match documented schemas; no undeclared fields (data exposure) or stack traces | Schemathesis checks, gateway response validation | CI and staging |
| Property-based and stateful fuzzing | Edge-case inputs do not cause 500s; chained operations do not leak other users' objects | Schemathesis stateful mode, Microsoft RESTler checkers | Staging only |
| Breaking change diff | A pull request does not silently drop a security requirement or widen a schema | Spec diff tools in CI, custom Spectral rules | Pull request |
| Drift detection | Traffic matches the published contract; undocumented endpoints are surfaced | Gateway logs or API discovery compared with the registry | Production, passively |
Schemathesis states in its own documentation that it sends real requests that create, change and delete data and should be pointed at development or staging, not production. RESTler describes itself as a stateful REST API fuzzer that compiles the description into a grammar and runs checkers for logic bugs such as resource leaks and hierarchy violations. Both are authorised-testing tools: run them against environments you own, with test accounts, and with rate limits set. For the fuzzing discipline itself, see the Fuzzing Guide.
Where identity fits: credentials in descriptions and test pipelines
OpenAPI work creates its own non-human identities. Contract test runners need API keys, client credentials or test user tokens for every scheme; documentation portals with "try it out" consoles hold OAuth client registrations; and generated SDKs often ship with example keys that were real once.
- Test credentials: issue dedicated, scoped client credentials per environment for contract testing, stored in the pipeline's secret store and rotated on a schedule. Never put working values in
exampleorexamplesfields. - Portal clients: register "try it out" consoles as public OAuth clients using authorization code with PKCE, with redirect URIs locked to the portal domain.
- Machine callers: describe machine-to-machine operations with
clientCredentialsormutualTLSrather than shared API keys, so each calling workload has its own identity. See the mTLS Guide.
Protecting and governing the description itself
The OWASP API Security Top 10 2023 entry for improper inventory management (API9) lists missing or outdated documentation as a "documentation blindspot" and recommends generating documentation from open standards in CI/CD and making it available only to those authorised to use the API. The OAS security considerations note that a Paths Object may be present but empty, so a portal can expose the Info Object to unauthenticated viewers while hiding operations.
- Single source of truth: store each description in the service repository, generate it from code or validate code against it, and publish to a registry from CI only. Hand-copied descriptions drift.
- Visibility tiers: public, partner and internal descriptions published from the same source with internal operations filtered out by tag or
x-internal, not maintained as separate files. - Do not serve internal specs from production: framework defaults often expose
/openapi.json,/swagger-uior/docs. Disable them on internet-facing hosts or put them behind authentication. The FastAPI Security Guide covers one common case. - Untrusted descriptions: when importing a partner's description into a gateway or code generator, lint it first (including the markdown rules), disable remote references and review generated code like any third-party dependency.
Rolling out contract-first API security
- Inventory: collect every OpenAPI description from repositories, gateways and portals into one registry; record APIs with no description as findings (API9).
- Baseline lint in report-only mode: run Spectral with
oasand the OWASP ruleset across the registry, publish counts per team and agree a severity policy. Nothing blocks yet. - Block new violations: fail pull requests on new errors only (missing write security, API keys in URLs, HTTP servers, undefined schemes). Existing findings go to a backlog with owners.
- Enforce from the contract: import descriptions into the gateway for request validation and authentication policy, starting with one API in monitor mode, then enforce. See the API Gateway Security Guide.
- Add contract tests: negative authentication tests for every secured operation, then response conformance, then stateful fuzzing in staging.
- Close the loop with runtime: compare gateway traffic with the registry weekly and open tickets for undocumented endpoints.
Rollback: keep gateway validation behind a per-API flag so a schema error that blocks legitimate traffic can be switched to monitor mode in minutes, and fix the description rather than the gateway rule.
Small versus large organisations: a small team can get most of the value from the OWASP ruleset in CI and a short list of negative authentication tests. A large estate needs a central registry, a shared organisational ruleset that extends the OWASP one with house rules (naming, error schema, required x-owner extension), and a platform team that owns the gateway import path.
Detection and measurement
- Log: gateway request validation failures by operation, 401 and 403 rates per operation, requests to paths not in the registry, and hits on documentation endpoints from outside expected networks.
- Metric: percentage of production APIs with a registered, linted description (target 100% for internet-facing).
- Metric: number of operations with no security requirement that are not on the approved public list (target zero).
- Metric: open OWASP ruleset errors per API and their trend.
- Metric: percentage of secured operations covered by an automated negative authentication test.
- Metric: undocumented endpoints observed in traffic per month, and median time to document or retire them.
Framework mapping
| Framework | Reference | How OpenAPI security supports it |
|---|---|---|
| OWASP API Security Top 10 2023 | API1, API2, API3, API4, API5, API8, API9 | Lint rules and schema constraints map directly (see the linting table) |
| OWASP ASVS 5.0 | V4.1.4, V8.1.1, V8.1.2, V13.4.5 | Allowed methods, documented authorisation rules, unexposed internal documentation |
| NIST CSF 2.0 | ID.AM-02, PR.PS-06 | Inventory of services; secure development integrated into the SDLC |
| NIST SP 800-53 Rev. 5 | CM-8, SA-11, SA-15 | Component inventory; developer testing; development standards and tools |
| ISO/IEC 27001:2022 Annex A | A.8.25, A.8.28, A.8.29 | Secure development life cycle, secure coding, security testing in development |
| MITRE ATT&CK | T1190 Exploit Public-Facing Application | Unauthenticated or unvalidated endpoints are the entry point this technique describes |
Common failure patterns
- Public documentation, unauthenticated endpoints: in the McKinsey Lilli incident, the researchers report finding API documentation for more than 200 endpoints publicly exposed, 22 of which accepted requests without authentication; one of those had a SQL injection flaw. A lint gate on unsecured operations and an access-controlled documentation portal address both halves.
- Security declared globally, removed locally: a developer adds
security: []to one operation to make a health check work, and the same edit is copied to a data endpoint. Allowlist anonymous operations by ID. - Contract and code disagree: the description says OAuth with a
writescope, the handler checks only that a token exists. Only negative contract tests catch this. - Unbounded schemas imported into the gateway: validation passes everything because no string has a
maxLengthand objects accept additional properties. - Swagger UI left on in production: framework default documentation endpoints expose internal and deprecated operations, feeding API9 shadow and zombie API problems.
- Real keys in examples: working API keys or tokens pasted into
examplefields and published to a portal or SDK. Scan descriptions with the same secret scanning as code.
Practitioner checklist
- Every API has one OpenAPI description in its repository, published to a central registry from CI.
- Spectral runs with the built-in
oasruleset and a pinned version of the OWASP API Security ruleset on every pull request. - No write operation lacks a Security Requirement; anonymous operations are on an approved list.
- No
apiKeyscheme usesin: query; noimplicitorpasswordOAuth flows on new APIs. - Strings, arrays and integers are bounded; write bodies use
additionalProperties: false. - 401, 403, 429 and 500 responses are defined with a fixed error schema.
- Negative authentication tests exist for every secured operation; fuzzing runs only in staging with test identities.
- Internal descriptions and documentation UIs are not reachable from the internet without authentication.
- Third-party descriptions are linted and remote references disabled before import.
- Gateway traffic is compared with the registry and undocumented endpoints are ticketed.
Standards and references
- OpenAPI Initiative: OpenAPI Specification 3.2.0
- OpenAPI Initiative: OpenAPI Specification 3.1.1
- OWASP: API Security Top 10 2023
- OWASP: Application Security Verification Standard 5.0
- OWASP Cheat Sheet Series: REST Security Cheat Sheet
- Stoplight: Spectral OpenAPI Rules
- Stoplight: Spectral OWASP API Security Ruleset
- Schemathesis: Project documentation
Related NHI Mgmt Group resources: API Security Guide · API Gateway Security Guide · Top 10 API Security Risks · Rate Limiting and API Abuse Guide · JWT Security Guide