Swagger UI exposed to unauthenticated users in all environments #6

Open
opened 2026-04-02 11:20:35 +02:00 by marcel · 5 comments
Owner

Problem

/swagger-ui/** and /v3/api-docs/** are configured as permitAll() in SecurityConfig, exposing the full API documentation to unauthenticated users in all environments including production.

Affected files

  • SecurityConfig.java:28

Attack scenario

An attacker accesses /swagger-ui/index.html and obtains a complete map of all endpoints, parameters, request/response schemas, and DTOs — significantly reducing reconnaissance effort.

Either:

  1. Gate behind a Spring profile: only permit in dev/test profiles, or
  2. Require ROLE_ADMIN authentication to access Swagger in production.

Severity

High — full API surface disclosed to unauthenticated users.

## Problem `/swagger-ui/**` and `/v3/api-docs/**` are configured as `permitAll()` in `SecurityConfig`, exposing the full API documentation to unauthenticated users in all environments including production. ## Affected files - `SecurityConfig.java:28` ## Attack scenario An attacker accesses `/swagger-ui/index.html` and obtains a complete map of all endpoints, parameters, request/response schemas, and DTOs — significantly reducing reconnaissance effort. ## Recommended fix Either: 1. Gate behind a Spring profile: only permit in `dev`/`test` profiles, or 2. Require `ROLE_ADMIN` authentication to access Swagger in production. ## Severity High — full API surface disclosed to unauthenticated users.
marcel added the kind/securitypriority/high labels 2026-04-02 11:21:02 +02:00
Author
Owner

👨‍💻 Kai — Frontend Engineer

Pure backend/security config change — no frontend code involved. But a couple of things worth thinking about from my side:

Developer experience during local development:

  • If Swagger is gated behind a Spring profile (dev/test only), it won't be available in production, which is the goal. But we should confirm our local dev setup (application-dev.yml or Docker Compose) always activates the dev profile so the team can still use Swagger locally without extra steps.
  • If the fix requires ROLE_ADMIN auth in all environments, developers will need to log in as an admin user before accessing Swagger — make sure the local seed data includes an admin user for this purpose.

API documentation alternatives:

  • Once Swagger is gated in production, the frontend team loses a convenient way to explore the API contract in a deployed environment. Is there a plan to export the OpenAPI spec as a static file (e.g., committed to the repo as openapi.yaml) so we can reference it without needing a running instance?
  • I'd find a committed openapi.yaml useful — it means I can validate request/response shapes during component development without hitting the backend.

Questions:

  • Which approach are we going with — profile-gated or ROLE_ADMIN required? Both have tradeoffs for the dev workflow.
  • Do we have an admin seed user in the local Docker Compose dev environment? If not, the ROLE_ADMIN approach creates friction for the whole team.
## 👨‍💻 Kai — Frontend Engineer Pure backend/security config change — no frontend code involved. But a couple of things worth thinking about from my side: **Developer experience during local development:** - If Swagger is gated behind a Spring profile (`dev`/`test` only), it won't be available in production, which is the goal. But we should confirm our local dev setup (`application-dev.yml` or Docker Compose) always activates the `dev` profile so the team can still use Swagger locally without extra steps. - If the fix requires `ROLE_ADMIN` auth in all environments, developers will need to log in as an admin user before accessing Swagger — make sure the local seed data includes an admin user for this purpose. **API documentation alternatives:** - Once Swagger is gated in production, the frontend team loses a convenient way to explore the API contract in a deployed environment. Is there a plan to export the OpenAPI spec as a static file (e.g., committed to the repo as `openapi.yaml`) so we can reference it without needing a running instance? - I'd find a committed `openapi.yaml` useful — it means I can validate request/response shapes during component development without hitting the backend. Questions: - Which approach are we going with — profile-gated or `ROLE_ADMIN` required? Both have tradeoffs for the dev workflow. - Do we have an admin seed user in the local Docker Compose dev environment? If not, the `ROLE_ADMIN` approach creates friction for the whole team.
Author
Owner

🔧 Backend Engineer — Spring Boot / PostgreSQL Specialist

Good catch — permitAll() on Swagger in production is a common oversight. The two proposed fixes have different tradeoffs, and I'd recommend option 1 (profile-gating) as the primary approach, with option 2 (admin auth) as a complement if we ever need Swagger accessible in production at all.

Preferred implementation — profile-gating:

// In SecurityConfig, conditionally include Swagger paths
@Value("${spring.profiles.active:prod}")
private String activeProfile;

// Or cleaner: use @ConditionalOnProperty / @Profile on the Swagger config bean itself

A cleaner approach: configure springdoc-openapi to only activate in dev and test profiles using springdoc.api-docs.enabled=false in application-prod.yml. This disables the OpenAPI endpoints entirely in production — no security rule needed because the paths don't exist.

SecurityConfig.java cleanup:

  • If Swagger is disabled via springdoc.api-docs.enabled=false, remove the permitAll() rule for /swagger-ui/** and /v3/api-docs/** from SecurityConfig entirely in the production config. A dead security exception that permits a non-existent path is confusing and a maintenance liability.

If ROLE_ADMIN approach is chosen instead:

  • The Swagger UI itself makes authenticated API calls from the browser. The session cookie approach works, but users need to be logged in via the app first. Make sure the Swagger UI is configured to send credentials (withCredentials: true in the Swagger config) — otherwise the API calls from the UI will be unauthenticated even if the user is logged in.

OpenAPI spec export:

  • Consider adding a CI step that runs mvn springdoc-openapi:generate (or equivalent) to export openapi.yaml as a build artifact. This gives the frontend team the contract without needing a running server.

Questions:

  • Is springdoc-openapi currently configured anywhere (e.g., SpringDocConfig.java)? If so, profile-gating at the bean level is the cleanest approach.
  • What is the active profile in the production deployment — is prod explicitly set, or is it running on the default profile?
## 🔧 Backend Engineer — Spring Boot / PostgreSQL Specialist Good catch — `permitAll()` on Swagger in production is a common oversight. The two proposed fixes have different tradeoffs, and I'd recommend option 1 (profile-gating) as the primary approach, with option 2 (admin auth) as a complement if we ever need Swagger accessible in production at all. **Preferred implementation — profile-gating:** ```java // In SecurityConfig, conditionally include Swagger paths @Value("${spring.profiles.active:prod}") private String activeProfile; // Or cleaner: use @ConditionalOnProperty / @Profile on the Swagger config bean itself ``` A cleaner approach: configure `springdoc-openapi` to only activate in `dev` and `test` profiles using `springdoc.api-docs.enabled=false` in `application-prod.yml`. This disables the OpenAPI endpoints entirely in production — no security rule needed because the paths don't exist. **`SecurityConfig.java` cleanup:** - If Swagger is disabled via `springdoc.api-docs.enabled=false`, remove the `permitAll()` rule for `/swagger-ui/**` and `/v3/api-docs/**` from `SecurityConfig` entirely in the production config. A dead security exception that permits a non-existent path is confusing and a maintenance liability. **If `ROLE_ADMIN` approach is chosen instead:** - The Swagger UI itself makes authenticated API calls from the browser. The session cookie approach works, but users need to be logged in via the app first. Make sure the Swagger UI is configured to send credentials (`withCredentials: true` in the Swagger config) — otherwise the API calls from the UI will be unauthenticated even if the user is logged in. **OpenAPI spec export:** - Consider adding a CI step that runs `mvn springdoc-openapi:generate` (or equivalent) to export `openapi.yaml` as a build artifact. This gives the frontend team the contract without needing a running server. Questions: - Is `springdoc-openapi` currently configured anywhere (e.g., `SpringDocConfig.java`)? If so, profile-gating at the bean level is the cleanest approach. - What is the active profile in the production deployment — is `prod` explicitly set, or is it running on the default profile?
Author
Owner

🧪 QA Engineer

Small security config change with targeted test needs. Here's the coverage I'd want:

Integration tests — profile-gating approach:

  • @ActiveProfiles("prod"): GET /swagger-ui/index.html → 404 (endpoint doesn't exist)
  • @ActiveProfiles("prod"): GET /v3/api-docs → 404
  • @ActiveProfiles("dev"): GET /swagger-ui/index.html → 200 (unauthenticated, dev is fine)
  • @ActiveProfiles("dev"): GET /v3/api-docs → 200

Integration tests — ROLE_ADMIN approach (if chosen instead):

  • Unauthenticated GET /swagger-ui/index.html → 401 or 302 redirect to login
  • Authenticated as ROLE_USER → 403
  • Authenticated as ROLE_ADMIN → 200
  • Unauthenticated GET /v3/api-docs → 401

Regression — existing SecurityConfig tests:

  • All other permitAll() rules (login, register, public endpoints) still work as expected after the change
  • The Swagger rule removal/change does not accidentally affect neighboring rules in the SecurityFilterChain — order of rules matters in Spring Security

Manual verification before closing:

  • Deploy to a staging/production-like environment with the prod profile active and manually verify Swagger is inaccessible — this is one of those fixes where I want human eyes confirming the actual deployed behavior, not just test assertions.

Edge cases:

  • What about /swagger-ui.html (the old redirect path that springdoc sometimes still serves)? Should that also be gated?
  • Does /webjars/** (used by Swagger UI for its assets) need to be addressed too?

Questions:

  • Do we have profile-specific integration test configurations already (@ActiveProfiles("prod") test slices)?
  • Is there a staging environment where we can verify the production profile behavior before this goes live?
## 🧪 QA Engineer Small security config change with targeted test needs. Here's the coverage I'd want: **Integration tests — profile-gating approach:** - `@ActiveProfiles("prod")`: GET `/swagger-ui/index.html` → 404 (endpoint doesn't exist) - `@ActiveProfiles("prod")`: GET `/v3/api-docs` → 404 - `@ActiveProfiles("dev")`: GET `/swagger-ui/index.html` → 200 (unauthenticated, dev is fine) - `@ActiveProfiles("dev")`: GET `/v3/api-docs` → 200 **Integration tests — ROLE_ADMIN approach (if chosen instead):** - Unauthenticated GET `/swagger-ui/index.html` → 401 or 302 redirect to login - Authenticated as `ROLE_USER` → 403 - Authenticated as `ROLE_ADMIN` → 200 - Unauthenticated GET `/v3/api-docs` → 401 **Regression — existing SecurityConfig tests:** - All other `permitAll()` rules (login, register, public endpoints) still work as expected after the change - The Swagger rule removal/change does not accidentally affect neighboring rules in the `SecurityFilterChain` — order of rules matters in Spring Security **Manual verification before closing:** - Deploy to a staging/production-like environment with the prod profile active and manually verify Swagger is inaccessible — this is one of those fixes where I want human eyes confirming the actual deployed behavior, not just test assertions. **Edge cases:** - What about `/swagger-ui.html` (the old redirect path that springdoc sometimes still serves)? Should that also be gated? - Does `/webjars/**` (used by Swagger UI for its assets) need to be addressed too? Questions: - Do we have profile-specific integration test configurations already (`@ActiveProfiles("prod")` test slices)? - Is there a staging environment where we can verify the production profile behavior before this goes live?
Author
Owner

🔐 Sable — Security Engineer

This is OWASP A05 (Security Misconfiguration) and correctly labeled high. An exposed Swagger UI in production is a gift to an attacker — it's the difference between hours of manual API discovery and having a complete, structured map handed to them in minutes.

Risk is broader than the issue describes:

  • The issue correctly notes endpoint enumeration. But Swagger also exposes request/response schemas and DTOs — so an attacker also learns your data model, field names, validation rules, and error formats. Combined with the stack trace leakage from issue #7, an attacker has everything they need for a targeted attack.

Recommended approach — defense in depth:

  1. Primary fix: Set springdoc.api-docs.enabled=false and springdoc.swagger-ui.enabled=false in application-prod.yml. This removes the paths entirely — no security rule needed because there's nothing to protect.
  2. Secondary fix: Remove the permitAll() rule for these paths from SecurityConfig when running with the prod profile. A permissive rule for a disabled endpoint is dead code that misleads future reviewers.
  3. Don't rely solely on SecurityConfig — if springdoc is still active and only the security rule changes, a misconfiguration (e.g., wrong profile active) re-exposes everything. Belt and suspenders.

Adjacent paths to audit:

  • /v3/api-docs/** (OpenAPI JSON) — directly machine-readable, arguably more dangerous than the UI
  • /swagger-ui.html — redirect path that some springdoc versions still serve
  • /webjars/** — Swagger UI static assets; if these are permitAll() they also leak the springdoc version (useful for CVE targeting)
  • /actuator/** — if exposed, as dangerous as Swagger. Worth auditing in the same pass.

Verify the production profile is actually active:

  • If the app is deployed without an explicit SPRING_PROFILES_ACTIVE=prod environment variable, it runs on the default profile — and the fix may not apply. Confirm the deployment configuration (Docker Compose, systemd unit, etc.) explicitly sets the active profile.

Questions:

  • Is SPRING_PROFILES_ACTIVE explicitly set in the production deployment configuration, or is the app running on the default profile?
  • Are actuator endpoints separately gated, or is this the first time we're reviewing what's exposed in production?
  • Has anyone confirmed whether this is currently exploitable in the production environment (i.e., can you actually reach /swagger-ui/index.html from outside the network)?
## 🔐 Sable — Security Engineer This is OWASP A05 (Security Misconfiguration) and correctly labeled high. An exposed Swagger UI in production is a gift to an attacker — it's the difference between hours of manual API discovery and having a complete, structured map handed to them in minutes. **Risk is broader than the issue describes:** - The issue correctly notes endpoint enumeration. But Swagger also exposes request/response schemas and DTOs — so an attacker also learns your data model, field names, validation rules, and error formats. Combined with the stack trace leakage from issue #7, an attacker has everything they need for a targeted attack. **Recommended approach — defense in depth:** 1. **Primary fix:** Set `springdoc.api-docs.enabled=false` and `springdoc.swagger-ui.enabled=false` in `application-prod.yml`. This removes the paths entirely — no security rule needed because there's nothing to protect. 2. **Secondary fix:** Remove the `permitAll()` rule for these paths from `SecurityConfig` when running with the prod profile. A permissive rule for a disabled endpoint is dead code that misleads future reviewers. 3. **Don't rely solely on `SecurityConfig`** — if springdoc is still active and only the security rule changes, a misconfiguration (e.g., wrong profile active) re-exposes everything. Belt and suspenders. **Adjacent paths to audit:** - `/v3/api-docs/**` (OpenAPI JSON) — directly machine-readable, arguably more dangerous than the UI - `/swagger-ui.html` — redirect path that some springdoc versions still serve - `/webjars/**` — Swagger UI static assets; if these are `permitAll()` they also leak the springdoc version (useful for CVE targeting) - `/actuator/**` — if exposed, as dangerous as Swagger. Worth auditing in the same pass. **Verify the production profile is actually active:** - If the app is deployed without an explicit `SPRING_PROFILES_ACTIVE=prod` environment variable, it runs on the default profile — and the fix may not apply. Confirm the deployment configuration (Docker Compose, systemd unit, etc.) explicitly sets the active profile. Questions: - Is `SPRING_PROFILES_ACTIVE` explicitly set in the production deployment configuration, or is the app running on the default profile? - Are actuator endpoints separately gated, or is this the first time we're reviewing what's exposed in production? - Has anyone confirmed whether this is currently exploitable in the production environment (i.e., can you actually reach `/swagger-ui/index.html` from outside the network)?
Author
Owner

🎨 Atlas — UI/UX Designer

This is a pure backend security configuration issue — no design work required, and I have no concerns about user-facing impact. The Swagger UI is a developer tool, not a user-facing screen, so gating or removing it has zero effect on any of our 18 app screens.

That said, a couple of observations that touch the broader product experience:

Internal tooling vs. end-user trust:

  • Swagger being public is also a trust signal issue — if a savvy user or journalist stumbles on /swagger-ui/index.html, they see a detailed technical map of our internal API. That's not the impression we want to give, even if it's not directly exploitable in their hands.

If we ever need a public-facing API docs surface:

  • For future reference: if the product ever needs a public developer-facing API documentation page (for integrations, webhooks, etc.), that should be a designed experience — not raw Swagger UI. We'd design a proper docs page using our design system, not expose the raw springdoc interface. So the profile-gating approach keeps that door appropriately closed for now.

No action items for me on this issue. Happy to design an admin-facing API explorer screen in a future v2 scope if there's ever a product need for it.

Questions:

  • Is there any v1 scope item that requires external developers to access the API? If not, profile-gating (disable in prod entirely) is the cleanest answer and I'd advocate for that over the ROLE_ADMIN approach.
## 🎨 Atlas — UI/UX Designer This is a pure backend security configuration issue — no design work required, and I have no concerns about user-facing impact. The Swagger UI is a developer tool, not a user-facing screen, so gating or removing it has zero effect on any of our 18 app screens. That said, a couple of observations that touch the broader product experience: **Internal tooling vs. end-user trust:** - Swagger being public is also a trust signal issue — if a savvy user or journalist stumbles on `/swagger-ui/index.html`, they see a detailed technical map of our internal API. That's not the impression we want to give, even if it's not directly exploitable in their hands. **If we ever need a public-facing API docs surface:** - For future reference: if the product ever needs a public developer-facing API documentation page (for integrations, webhooks, etc.), that should be a designed experience — not raw Swagger UI. We'd design a proper docs page using our design system, not expose the raw springdoc interface. So the profile-gating approach keeps that door appropriately closed for now. **No action items for me on this issue.** Happy to design an admin-facing API explorer screen in a future v2 scope if there's ever a product need for it. Questions: - Is there any v1 scope item that requires external developers to access the API? If not, profile-gating (disable in prod entirely) is the cleanest answer and I'd advocate for that over the `ROLE_ADMIN` approach.
Sign in to join this conversation.