Swagger docs public in production: how to close them
Swagger docs that anyone can open in production show every route of your API, so turn the docs routes off in production, or put them behind a login, and then test every route without a token. The docs page does not break into your app. It gives an attacker a complete map of the app, and that map saves the attacker a lot of time.
This guide shows how to confirm the problem, how to close it in FastAPI and Express, and how to check your logs. It also shows how to keep the docs closed after each deploy.
What does “Swagger docs public in production” mean?
It means that a visitor without an account can open the interactive API documentation of your live app. The documentation is built from an OpenAPI schema. The schema is a JSON file that lists each route, each parameter, and each response.
Many frameworks turn the docs on by default. FastAPI is one example. The FastAPI documentation says that Swagger UI is served at /docs, ReDoc is served at /redoc, and the schema is served at /openapi.json.
AI coding tools often create the problem. You ask the tool for an API. The tool creates the app with the default settings. The docs work well in development, so nobody turns them off before the first deploy.
The schema file is the more important target. Swagger UI is only a viewer for that file. If you close /docs but leave /openapi.json open, the attacker still gets the full list of routes.
Why does a public API map matter?
The docs do not give access to your data. The risk is the information that they give. A public schema shows an attacker:
- All routes, including admin, debug, and internal routes that no page of your app links to.
- The names of all parameters, including fields that the app should not accept from users.
- The format of each request, so the attacker can write correct requests at once.
- The example values. Some examples contain real emails, IDs, or keys.
- The auth scheme of each route. Routes without an auth marker are the first targets.
An attacker who guesses routes needs many requests, and many requests make noise in your logs. An attacker who reads the schema needs a few requests. The docs remove that effort.
Note: Public docs do not cause broken access control. They make it easier to find. If one route has no auth check, the docs show that route to every visitor. See authorization patterns for how to check each route.
There is a second risk. Swagger UI has an Authorize button. A developer can type a token in the button during a test. The setting persistAuthorization keeps that token in the browser. The Swagger UI configuration reference states that the setting persists authorization data and that the data is not lost when the browser closes or refreshes. If the page is public, the stored token belongs to a real account. The pattern page on API docs exposure describes this case.
How do I check if my Swagger docs are public?
Request the common docs paths from a machine that has no login. Use curl and do not send cookies or tokens.
BASE=https://your-app.example
for p in /docs /redoc /openapi.json /swagger /swagger.json /swagger-ui /swagger-ui.html /api-docs /v3/api-docs /api/docs; do
code=$(curl -s -o /dev/null -w "%{http_code}" "$BASE$p")
echo "$code $p"
done
A 200 code on any path means that the path is public. A 301 or 302 code can lead to a docs page, so follow the redirect with curl -L and check again. A 401, 403, or 404 code means that the path is closed for this test.
Some apps serve the docs on the API host, not on the web host. If your frontend calls api.your-app.example, run the same loop for that host too.
Then read the schema. If /openapi.json answers, list the routes:
curl -s "$BASE/openapi.json" | python3 -c "import sys,json; print('\n'.join(json.load(sys.stdin)['paths']))"
Look for routes that contain admin, internal, debug, export, user, or key. Also search the schema for real values:
curl -s "$BASE/openapi.json" | grep -i -E "bearer|api[_-]?key|secret|password|@"
Warning: Test only your own app. Do not run these requests against an app that you do not own.
You can also open the docs page in a private browser window. Click Authorize and check if any token is already filled in. A filled field means that a person stored a token in a public page.
How do I close the docs in production? Fix it now, in order of urgency
Do the steps in this order. Step 1 limits the damage. The other steps remove the cause.
- Rotate any token that was typed into the public docs page. If you or a tester used the Authorize button on the live app, create a new token and revoke the old token. Do this first, because a stored token is a working login.
- Remove real values from the schema. Search the route descriptions and the examples. Replace real emails, IDs, and keys with fake values.
- Test each route without a token. Use the list from the schema. Each route that must need a login must return
401or403. Fix any route that returns data. This step matters more than closing the docs, because the routes stay reachable after you close the docs. - Turn the docs off in production. Use the code in the next section.
- Deploy the change. The old setting stays active until the new build is live.
- Run the check from the previous section again. All paths must return
404or401.
Caution: If other tools read your schema, they stop working after step 4. Examples are a generated client, a contract test, and a partner integration. Make a copy of the schema in your repository first. Then use the copy in these tools.
FastAPI
FastAPI reads the docs paths when you create the app. The FastAPI documentation states that you can disable each path with None. Setting openapi_url=None disables the schema and the docs pages that use it.
import os
from fastapi import FastAPI
is_prod = os.getenv("APP_ENV") == "production"
app = FastAPI(
docs_url=None if is_prod else "/docs",
redoc_url=None if is_prod else "/redoc",
openapi_url=None if is_prod else "/openapi.json",
)
Set APP_ENV=production in the environment variables of your host. Make sure that the variable exists. If the variable is missing, the code keeps the docs on, so the default is not safe. You can reverse the check: turn the docs on only when APP_ENV equals development.
Express
Mount the docs only outside production. This example uses the swagger-ui-express package. Mount the schema route in the same block.
import express from "express";
import swaggerUi from "swagger-ui-express";
import spec from "./openapi.json" with { type: "json" };
const app = express();
if (process.env.NODE_ENV === "development") {
app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec));
app.get("/openapi.json", (req, res) => res.json(spec));
}
The condition tests for development, not for “not production”. A staging server then also keeps the docs closed. Other frameworks use other settings, but the rule is the same: the docs code must not run when the variable is missing or has a different value.
Keep the docs, but add a login
Sometimes you need the docs on a live host. In this case, put the docs behind your own login and an admin check. Do not use the same check as the public users. Use the admin check that you use for other admin pages.
app.use("/docs", requireLogin, requireAdmin, swaggerUi.serve, swaggerUi.setup(spec));
The names requireLogin and requireAdmin are placeholders. Use the middleware of your auth library. Protect the schema route in the same way. If you leave /openapi.json open, the login on /docs does not help.
Also turn off persistAuthorization in the Swagger UI options. Then the page does not store tokens in the browser.
How do I check if someone used the docs?
Read the access logs of your host for the period in which the docs were public. You cannot prove that nobody read the docs, but you can find signs of use.
- Find the requests to the docs paths. Search the logs for
/docs,/redoc,/openapi.json,/swagger, and/api-docs. Your own requests also appear, so remove the IP addresses that you know. - Look at the sequence. An attacker usually requests the schema once. Then the same address requests many routes that you do not use in the frontend. Such a pattern means that someone followed the map.
- Look at routes that no page of your app calls. Compare the routes in the logs with the routes that your frontend uses. Requests to the other routes from unknown addresses need a closer look.
- Check for errors. A burst of
401,403,422, and500responses from one address shows that someone tested your routes with wrong input. - Check for successful calls to sensitive routes. Search for
200responses on admin, export, and user routes from addresses that are not yours. - Check the account of any stored token. If a token was in the public page, read the activity of that account. Look for logins and changes that the owner did not make.
Where you find the logs depends on your host. Vercel, Netlify, Railway, and Render each have a log page in the dashboard. Some plans keep logs for a short time only. Download the logs now if you need them later.
If you find a successful request to a route that should be closed, your problem is larger than the docs. Read API security basics and check that route first.
How do I stop it from happening again?
Use these controls so that the docs stay closed after each deploy.
- Use one environment variable for the docs. Do not edit the code for each deploy. A single
APP_ENVvalue must control the docs, the debug mode, and the log level. - Make the safe state the default. The code must turn the docs on only when the variable equals
development. A missing variable then keeps the docs closed. - Add a test to your deploy. After each deploy, run the
curlloop from this guide against the live URL. Make the build fail if any docs path returns200. - Keep a reviewed copy of the schema in your repository. Generate the client and the contract tests from this copy. Do not read the schema from the live server.
- Add a rule for your coding tool. Write in the project rules: “Disable the API docs routes when
APP_ENVisproduction.” Check that the generated code follows the rule. - Write real auth on each route, not only on the page. A closed docs page is a small gain. A route with a check for the user and for the owner of the data is the real control. See BOLA in AI-generated CRUD for the most common mistake.
- Check the other debug surfaces. Docs pages are not the only tool that stays on after development. Health pages, metrics pages, and admin panels can also be open. The production security checklist lists them.
If you want public docs for customers, publish a separate file. Create it from a copy of the schema that contains only the public routes. Host it as a static page. Then the live app does not serve any schema, and the customers still read the docs.
When should I get help?
Get help in these cases:
- Your logs show a successful request to an admin route or an export route from an address that is not yours.
- A token or an API key was in the public page or in the schema, and you cannot tell who used it.
- You find routes that return data without a token, and you do not know how to add the check in a safe way.
- Your app stores personal data and the logs show that someone tested many routes. You may have a duty to report a breach.
- You cannot find where the docs routes enter the build. A fresh scan of the deployed app shows what each path returns.
After you finish, run the curl loop one more time on the live host. Then open each important route without a token, and confirm that the app refuses the request.
Common questions
Is a public Swagger page a vulnerability by itself?
Does a hidden docs URL protect my API?
Can I keep the docs public for my API customers?
I turned off the docs. Is my API safe now?
What if someone typed a token into the public Swagger page?
Find open API docs
A VibeEval scan requests the common Swagger, ReDoc, and OpenAPI paths on your deployed app and reports the ones that answer without a login.
14-day free trial · No credit card · Cancel anytime