~/blog/openapi-docs-public-endpoint-mapPOSTED
by Shady Nathan Tawfik · August 22, 2026 · 4 min

The API Documentation Was Public, And It Was The Best Map You Can Ask For

An application built with an automated framework served interactive documentation and a machine-readable schema at a well-known path. Both were enabled by default and neither was behind a login.

Interactive[blog-2.0]

TL;DR: The API documentation was public, and it was the best map you can ask for: every path, field name and validation rule, including the administrative routes no interface links to. I show how to disable it outside development.

The scenario

I ran this as a black-box review: no credentials, only the publicly visible surface. Here is what I saw before I touched anything.

A software product built with an automated application framework, with a separate client-side frontend, third-party services for payments, artificial intelligence and mail, and object storage for media. Assessment covered the public schema and documentation endpoints, header policy, script integrity, mail authentication and the trust chain.

I removed all identifying information. The target is described only by sector and technology class so the pattern can be reused.

The pattern

The pattern I recognised — and that I keep finding in similar estates:

Frameworks that generate a client from a type definition will happily generate documentation from the same definition. This is a feature: the documentation cannot drift from the implementation, because both come from one source. The consequence is that publishing the documentation is a single environment decision, and the default in several popular frameworks is to publish it in every environment including production.

An interactive documentation page is more than a convenience. It renders the real schema, with field names, types, required flags and validation rules, which is the information an attacker needs to build correct requests rather than guessing parameter names. The machine-readable schema behind it is better still, because it enumerates every path including administrative and internal ones that no user interface ever links to.

The deeper pattern is default configuration. Every control in this engagement was a framework default rather than a decision: documentation on, inline scripts allowed in the policy, analytics loaded from a self-hosted instance on a separate server, media served from a storage endpoint with a wildcard origin policy. None of these are errors in the sense that somebody got them wrong. They are the shape of the framework, and the review has to look at defaults precisely because nobody chose them.

TL;DR: An application built with an automated framework served interactive documentation and a machine-readable schema at a well-known path. Both were enabled by default and neither was behind a login.

Findings

SeverityFindingEvidence
MEDIUMInteractive documentation publicly servedFull schema with field names and validation rules
MEDIUMMachine-readable schema lists internal and administrative pathsComplete route inventory including paths no interface links to
MEDIUMInline scripts permitted in the content security policyNo restriction on inline execution
MEDIUMThird-party script loaded without integrity and from a separate hostCompromise of that host means full control of the page
LOWMedia endpoint serves with a wildcard origin policyAny origin may read stored files
LOWTransport policy missing the preload and subdomain scopeHardening present but incomplete

The attack path

Application type definitionsGenerated clientGenerated documentationPublished by default in productionFull route inventoryCorrect requests built without guessing

Reproduction in my lab

Every command below targets a lab container I control. Nothing here is aimed at a live system.

Check the well-known documentation paths on your own service

~/code/bash bash
# target: your own lab service on localhost
$ for p in /docs /redoc /openapi.json /api-docs /swagger /swagger.json; do
    printf '%-16s %s\n' "$p" "$(curl -so /dev/null -w '%{http_code}' "http://localhost:8000$p")"
done

# if the schema is public, read the route inventory from it
$ curl -s http://localhost:8000/openapi.json | python3 -c '
import json,sys
spec = json.load(sys.stdin)
for path, ops in spec.get("paths", {}).items():
    print(path, ",".join(k.upper() for k in ops))' | head -40

Detection in your own estate

Look for inline execution allowances in your own policy

~/code/bash bash
$ curl -sI https://example.org/ | grep -i 'content-security-policy' | tr ';' '\n' \
  | grep -E 'script-src|unsafe-inline|unsafe-eval|\*'

List every third-party script origin your page loads

~/code/bash bash
$ curl -s https://example.org/ | grep -oE '<script[^>]*src="https?://[^"]*"[^>]*>' \
  | grep -oE 'src="https?://[^/]+' | sort -u

# and how many of them carry integrity
$ curl -s https://example.org/ | grep -oE '<script[^>]*src="https?://[^"]*"[^>]*>' \
  | grep -c integrity

Remediation

Disable documentation outside development

~/code/python python
# FastAPI: the documentation is opt-in, so make production opt-out
import os

ENV = os.environ.get("APP_ENV", "production")

app = FastAPI(
    title="Internal service",
    # both default to True; leaving them unset publishes the schema
    docs=None if ENV == "production" else "/docs",
    redoc=None if ENV == "production" else "/redoc",
    openapi_url=None if ENV == "production" else "/openapi.json",
)

# and assert it in a test, so a future default change cannot reintroduce it
def test_no_schema_in_production(client):
    app.openapi_url = None
    assert client.get("/openapi.json").status_code == 404
    assert client.get("/docs").status_code == 404

Once an attacker gains access, they can perform the following actions:

Interactive documentation publicly served
Machine-readable schema lists internal and administrative paths
Inline scripts permitted in the content security policy
Third-party script loaded without integrity and from a separate host

Takeaways

  • A generated schema is an attack surface map that costs one request.
  • Documentation is opt-in in the framework and opt-out in your environment. Set it explicitly.
  • Assert the absence in a test; a default change will not announce itself.
  • Audit the third-party script origins. Owning your own server is not the same as controlling it.

For educational purposes only. Every reproducible command targets my own lab environment (localhost), never a live system. © 2026