AI 코딩 보안 2026.11.10 · 조회 11

Swagger 운영 노출: FastAPI docs 비활성화와 openapi.json 점검 순서 [2026]

FastAPI 는 앱을 만들면 /docs, /redoc, /openapi.json 이 기본으로 열린다. 운영에 그대로 배포하면 경로, 파라미터, 인증 방식이 적힌 API 지도가 공개된다. 노출 자체가 침해는 아니지만 공격 준비 시간을 줄이고 숨겨 둔 관리용·구버전 경로를 드러낸다. 문서를 끄는 법, 인증 뒤로 옮기는 코드, 권한 검사까지 확인하는 순서를 정리했다.

S
보안 전문 기업 SENTRIX 레드팀
해킹대회 수상 · 보안 실무 10년 · 실전 진단 경험을 바탕으로 작성합니다.
웹사이트를 운영하고 있다면
보안 헤더·인증서·공개 노출부터 무료로 점검하세요. 결과 링크는 이메일로 받습니다. 로그인 이후 권한·업무 로직은 별도 검증이 필요합니다.

다음 화면에서 이메일과 점검 권한을 확인합니다. 회원가입·카드 없이 무료 요약을 받고, 상세 리포트는 선택 구매합니다.

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 이 그대로 열림중간
2docs_url 과 redoc_url 만 끄고 openapi_url 은 남겨 /openapi.json 에 API 명세 전체가 남음높음
3include_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 보안 상담으로.

관련 글

내 사이트도 점검해보세요

URL 입력, 결과 링크는 메일로 받는 무료 점검. 회원가입·카드 불필요.

AI 코딩 보안 체크리스트 받기

AI 코딩 보안 체크리스트(PDF)를 무료로 받아보세요.

코드 자체가 걱정이라면 : 소스코드 보안 분석(SAST)이 ZIP 업로드만으로 취약점 위치와 수정 코드를 찾아드립니다.
이런 글을 계속 받아보려면 뉴스레터 구독 · RSS 피드