Swagger·OpenAPI 문서 운영 노출은 개발 편의용 자동 API 문서(Swagger UI, ReDoc, openapi.json)가 운영 서버에서도 인증 없이 열려, 엔드포인트와 파라미터와 인증 방식이 적힌 API 지도를 누구나 받아 갈 수 있는 상태다.
FastAPI 는 앱을 만들기만 해도 /docs(Swagger UI), /redoc, /openapi.json 이 기본 주소로 열리고(공식 문서), Spring Boot 용 springdoc 도 의존성만 추가하면 별도 설정 없이 문서를 만든다. AI 코딩 도구가 짜 준 API 서버를 /docs 화면에서 눌러 보며 확인하는 흐름이 자연스럽다 보니, 이 화면을 끄는 일은 따로 챙기지 않으면 빠진 채 운영에 올라간다. 노출 자체가 침해는 아니지만 OpenAPI 문서에는 경로, 파라미터, 인증 방식, 서버 주소가 기계가 읽는 형식으로 정리돼 있어서, 공격자가 직접 찾아야 할 목록을 대신 만들어 주고 화면에 링크가 없던 관리용·구버전 경로까지 드러낸다. OWASP API Security Top 10(2023)도 API 문서를 API 사용 권한이 있는 사람에게만 제공하라고 권고한다. 외부 개발자에게 공개하려고 만든 API 는 문서 공개가 의도된 것이므로, 이 글은 자기 서비스의 화면이나 앱만 쓰는 내부용 API 의 문서가 의도 없이 열린 경우를 다룬다.
이 글은 자주 발생하는 실수 패턴, 점검·조치 절차, 자가진단을 정리했다.
한눈에 보는 핵심
Swagger 문서가 열려 있으면 바로 뚫리는 건가요?
그렇지 않습니다. 문서에는 경로와 요청 형식이 적혀 있을 뿐 서비스가 저장한 데이터가 나오지는 않아서 노출 자체가 침해는 아닙니다. 다만 공격자가 엔드포인트를 하나씩 찾는 수고를 덜어 주고, 권한 검사가 빠진 관리용 경로가 있으면 바로 시험할 목록이 됩니다. PortSwigger 의 API 점검 학습 자료도 /openapi.json, /swagger/index.html 같은 경로부터 찾아보라고 안내합니다.
FastAPI 에서 /docs 와 openapi.json 을 끄려면 어떻게 하나요?
FastAPI() 를 만들 때 docs_url=None, redoc_url=None, openapi_url=None 을 모두 주면 됩니다. 공식 문서는 openapi_url=None 이 이를 쓰는 문서 화면까지 함께 끈다고 설명하지만, docs_url 과 redoc_url 만 끄면 openapi.json 이 남습니다. 개발 환경을 명시했을 때만 문서가 열리도록 환경변수로 나누는 편이 안전합니다.
문서만 끄면 API 도 안전해지나요?
아닙니다. 문서는 지도이지 문이 아니어서, 끄면 지도를 구하기 어려워질 뿐 엔드포인트는 그대로 응답합니다. FastAPI 의 include_in_schema=False 도 공식 문서 설명대로 OpenAPI 스키마와 자동 문서에서 빼는 옵션이지 접근을 막는 옵션이 아닙니다. 관리용 경로는 문서가 있든 없든 서버에서 인증과 권한 검사로 막아야 합니다.
자주 발생하는 실수 패턴
| # | 패턴 | 위험도 |
|---|---|---|
| 1 | 운영 서버에서도 FastAPI() 를 기본 인자로 둬 /docs, /redoc, /openapi.json 이 그대로 열림 | 중간 |
| 2 | docs_url 과 redoc_url 만 끄고 openapi_url 은 남겨 /openapi.json 에 API 명세 전체가 남음 | 높음 |
| 3 | include_in_schema=False 로 문서에서만 숨기고 해당 엔드포인트의 인증·권한 검사는 넣지 않음 | 높음 |
| 4 | 문서 주소만 /api/v1/openapi.json 처럼 바꾸고 접근 통제는 없음 (찾기가 조금 어려워질 뿐) | 중간 |
| 5 | 환경변수가 없을 때 개발 모드로 동작하게 짜서, 변수를 빠뜨린 서버에서 문서가 열림 | 높음 |
| 6 | 문서에 남은 구버전·미사용 경로(/v1/ 등)가 서버에서는 아직 응답함 | 높음 |
운영에서 문서 끄기, 필요하면 인증 뒤로 옮기기
import os
import secrets
from fastapi import Depends, FastAPI, HTTPException
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.security import HTTPBasic, HTTPBasicCredentials
# 개발 환경으로 명시했을 때만 문서를 연다. 변수가 없거나 오타여도 닫히는 쪽으로 실패한다
SHOW_DOCS = os.getenv('APP_ENV') in ('dev', 'local')
# 1) 그 밖의 환경에서는 Swagger UI, ReDoc, openapi.json 3종을 모두 끈다
app = FastAPI(
docs_url='/docs' if SHOW_DOCS else None,
redoc_url='/redoc' if SHOW_DOCS else None,
openapi_url='/openapi.json' if SHOW_DOCS else None,
)
# 2) 운영에서도 팀이 문서를 봐야 하면 인증을 통과한 요청에만 다시 연다
basic = HTTPBasic()
def same(a, b):
# compare_digest: 비교 시간 차이로 값을 추측하는 공격을 막는다 (bytes 로 바꿔 비교)
return secrets.compare_digest(a.encode('utf8'), b.encode('utf8'))
def docs_guard(cred: HTTPBasicCredentials = Depends(basic)):
if not (same(cred.username, os.environ['DOCS_USER'])
and same(cred.password, os.environ['DOCS_PASS'])):
raise HTTPException(status_code=401, detail='Unauthorized',
headers={'WWW-Authenticate': 'Basic'})
# 계정 환경변수가 둘 다 있을 때만 문서 경로를 만든다 (HTTPS 뒤에서만 사용)
if not SHOW_DOCS and os.getenv('DOCS_USER') and os.getenv('DOCS_PASS'):
@app.get('/openapi.json', include_in_schema=False, dependencies=[Depends(docs_guard)])
def protected_schema():
return app.openapi()
@app.get('/docs', include_in_schema=False, dependencies=[Depends(docs_guard)])
def protected_docs():
return get_swagger_ui_html(openapi_url='/openapi.json', title='API docs')
로컬에서는 APP_ENV=dev 로 띄워야 문서가 보이고, 변수가 없거나 값이 달라도 닫히므로 설정 실수가 노출로 이어지지 않는다.
Spring 은 springdoc.swagger-ui.enabled=false 와 springdoc.api-docs.enabled=false 를 둘 다 적용하고, NestJS 는 SwaggerModule.setup 호출을 운영이 아닐 때만 실행하게 감싼다.
문서를 끄는 일은 지도를 치우는 것이고, 문은 엔드포인트마다 서버에서 거는 인증과 권한 검사다.
자가진단
# 문서를 일부러 공개하는 API(외부 개발자용)는 열려 있는 것이 정상이다. 아래는 내부용 API 기준이다.
# bash 기준이다 (Windows 는 Git Bash 또는 WSL). your.app 은 본인 주소로 바꾼다.
# 1) 흔한 문서 경로의 상태 코드
for p in /docs /redoc /openapi.json /v3/api-docs /swagger-ui.html /api-json /api/docs /api/swagger /api/openapi.json; do
echo $p $(curl -s -o /dev/null -w '%{http_code}' https://your.app$p)
done
# 200 이면 의심, 404 면 없음, 401 이나 403 이면 인증 뒤에 있다.
# 경로를 바꿔 뒀다면 코드의 openapi_url 값이나 springdoc.api-docs.path 값을 목록에 더한다.
# 없는 경로에도 200 을 돌려주는 화면 앱(SPA)이 있으니 2단계로 본문을 확인한다.
# 2) 200 이 나온 경로의 본문 확인
curl -s https://your.app/openapi.json | head -c 300
curl -s https://your.app/docs | grep -i -m1 swagger-ui
# 첫 명령에서 openapi(값 3.x) 또는 swagger(값 2.0) 키와 paths 가 보이거나, 둘째 명령에서 swagger-ui 가 걸리면 노출 확정이다.
# 둘 다 아니고 HTML 첫 화면이 나오면 노출이 아니다.
# 3) 문서 안에 무엇이 적혀 있는지 읽는다 (jq 필요)
curl -s https://your.app/openapi.json | jq -r '.paths | keys[]' | grep -iE 'admin|internal|debug|export|/v[0-9]+/'
curl -s https://your.app/openapi.json | jq '.servers, .components.securitySchemes'
# admin, internal 같은 이름이나 /v1/ 같은 구버전이 나오면 4, 5단계로 간다.
# servers 에 내부 주소, description 이나 example 에 실제 토큰·테스트 계정이 있으면 지우고, 토큰은 폐기 후 재발급한다.
# 4) 3단계에서 찾은 관리용 경로를 토큰 없이 호출 (자기 서비스에서만, GET 만 시험한다)
echo $(curl -s -o /dev/null -w '%{http_code}' https://your.app/<관리용 경로>)
# 401 이나 403 이면 정상, 200 이면 문서 노출보다 권한 검사 누락이 먼저다.
# 5) 구버전 경로가 아직 응답하는지
echo $(curl -s -o /dev/null -w '%{http_code}' https://your.app/<구버전 경로>)
# 쓰지 않는 버전이 200 이면 라우터를 코드에서 지운다.
# 6) 조치 후 1단계를 다시 실행한다
# 모두 404 로, 인증 뒤로 옮겼다면 401 로 바뀌어야 한다.
위 시그널이 발견되면 결함이다. FastAPI 공식 문서(docs_url·redoc_url·openapi_url 설정)를 함께 참조하라.
조치 절차
| 순위 | 조치 |
|---|---|
| 1 | 자가진단 1~3단계로 Swagger UI, ReDoc, openapi.json 이 각각 어떻게 응답하는지 확인한다. 화면(UI)이 아니라 openapi.json 부터 본다. UI 가 닫혀 있어도 명세 파일이 열려 있으면 지도는 그대로 공개된 상태다 |
| 2 | 운영에서 문서를 끈다. FastAPI 는 docs_url, redoc_url, openapi_url 을 모두 None 으로, Spring(springdoc)은 springdoc.swagger-ui.enabled=false 와 springdoc.api-docs.enabled=false 를 둘 다, NestJS 는 SwaggerModule.setup 호출을 운영에서 뺀다. 개발 환경을 명시했을 때만 열리게 하고 변수가 없으면 닫는다 |
| 3 | 운영에서도 문서가 필요하면 인증 뒤로 옮긴다. 앱 안에서는 문서 경로에 인증 의존성을 붙이고, 리버스 프록시가 있으면 경로 단위로 IP 허용 목록이나 SSO 를 건다. 주소만 바꾸는 것은 접근 통제가 아니다 |
| 4 | 문서에 나열된 관리용·내부용 경로를 토큰 없이 호출해 401, 403 이 나오는지 확인하고, 서버 코드에서 엔드포인트마다 인증과 권한 검사를 둔다. include_in_schema=False 로 문서에서 숨기는 것은 통제가 아니다 |
| 5 | 더는 쓰지 않는 구버전·테스트 경로는 코드에서 지운다. 남기는 버전은 어느 환경에서 누가 접근하는지와 폐기 일정을 적어 둔다 (OWASP API9 의 인벤토리 기준) |
| 6 | 문서의 description, example, servers 칸에 토큰, 내부 주소, 테스트 계정이 없는지 읽어 보고, 운영 배포 직후 1단계 확인이 자동으로 돌도록 배포 절차에 넣는다 |
지금 해야 할 것
실무자: 위 curl 로 /docs, /redoc, /openapi.json 응답부터 직접 확인하고, API 서버 주소를 CodeScan 무료 스캔으로 점검한다. 무료 점검이 보는 문서 경로는 /api/docs, /api/swagger, /api/openapi.json 세 개이고 상태 코드 200 여부로 판정하므로, 기본 경로와 본문 확인, 엔드포인트별 권한 검사는 자가진단으로 채운다.
CISO·임원: 문서에 나열된 관리용 엔드포인트의 접근 제어처럼 자동 점검이 닿지 않는 부분은 SENTRIX 보안 상담으로.