최규선.com(Flask·Fly.io·SQLite)에 OWASP API Security Top 10(2023)을 항목별로 대입해 점검하고, 취약 6건·위험 3건을 조치한 뒤 읽기 전용 점검 도구 api-sec-check로 충족 24·주의 2·미충족 0을 확인한 기록임.
GitHub: kh20134/owasp-api-top10-kyusun
개요
- 대상: 본인 블로그 최규선.com. Flask 앱, Fly.io 머신 1대, SQLite
/data/blog.db - 기준: OWASP API Security Top 10 (2023)
- 범위: 공개 페이지와 관리자 라우트. 게시글 본문과 DB 데이터는 변경하지 않음
- 판정: 양호 / 위험 / 취약 3단계
- 원칙: 일반 방문과 정상 관리 흐름은 그대로 유지한 상태에서 조치함
- 실제 반영: 블로그 저장소 PR #37(병합 커밋
a62efc8)에서 항목별 조치, PR #38에서 CSPimg-src보완
왜 API 기준으로 봤나
- 블로그도 결국 HTTP 엔드포인트 집합임. 공개 GET 7개, 관리자 라우트 11개, JSON 엔드포인트 1개(카테고리 정렬)
- 웹 Top 10은 지난 글에서 실습 저장소로 다룸. 이번에는 운영 중인 본인 사이트에 API 기준을 직접 대입함
- 관련 글: OWASP Top 10 공격기법 및 대응방안
점검 결과 요약
- 조치 전: 취약 6건(API2·4·5·6·8·10), 위험 3건(API1·3·9), 양호 1건(API7)
- 조치 후: 양호 5건(API1·3·5·7·9), 조치 완료 5건(API2·4·6·8·10)
- 방문자 영향: HTML 본문, 글 주소, 사이트맵은 동일. 바뀐 것은 응답 헤더, 스크립트 출처, 관리 폼의 CSRF 숨은 필드뿐임
- 관리자 영향: 배포 직후 쿠키 이름이
blog_session으로 바뀌어 한 번 재로그인. 로그인 유지 8시간
구조도
요청은 브라우저 → Fly 가장자리(TLS 종료) → Flask 보안 미들웨어(헤더·CSP, CSRF, 속도 제한, 인가, 입력 허용 목록) 순으로 들어가 SQLite 볼륨과 /static/vendor/에 닿음. 파선은 신뢰 경계임. (출처: 본인 깃허브)
- 신뢰 경계 1: 브라우저. 공개 GET과 관리 화면
- 신뢰 경계 2: Fly edge.
FLY_APP_NAME이 있을 때만 프록시 한 홉 신뢰 - 신뢰 경계 3: Flask 프로세스. 미들웨어를 통과해야 DB와 정적 자산에 접근
API1. Broken Object Level Authorization
- 개념: 객체 식별자로 데이터에 닿는 기능마다 그 객체에 대한 인가 확인 필요. 식별자만으로 다른 객체에 닿으면 BOLA임
- 위험성: 비공개 객체나 타인 소유 객체가 번호 변경만으로 열릴 수 있음
- 적용 전 상태: 위험(낮음)
- 공개 조회
get_post는published=1만 조회. 비공개 글은 404 - 관리 객체 접근은
session["admin_ok"]불리언만 확인. 플래그만 있으면 모든 글·카테고리 ID 도달 - 계정이 하나라 가로 월권은 없으나, ID가 순번이라 존재 여부는 추측 가능
- 공개 조회
- 조치 내용
admin_session_ok()가admin_ok와admin_user == ADMIN_USERNAME을 함께 확인- 비교는
hmac.compare_digest - 없는 글 수정은 404. 비로그인 삭제는 302로 로그인에 보내고 DB는 그대로 둠
- 검증:
test_api1_unpublished_object_is_hidden_and_admin_can_open_it. 조치 후 양호
API2. Broken Authentication
- 개념: 인증이 잘못 붙으면 세션·토큰 탈취나 다른 자격으로의 접속이 가능해짐
- 위험성: 실패 횟수 제한 없는 로그인, 공개된 서명 키, 고정된 세션은 관리자 세션 탈취로 이어짐
- 적용 전 상태: 취약
FLASK_SECRET_KEY가 없으면 코드에 적힌 공개 기본값으로 쿠키 서명- 로그인 실패 횟수 제한 없음
- 로그인 성공 시 세션을 비우지 않음(세션 고정 가능)
- 쿠키
Secure가 기동 시점 환경변수에만 의존
- 조치 내용
- IP당 실패 8회/10분 초과 시 429,
Retry-After: 60 - 아이디·비밀번호 모두 상수 시간 비교
- 성공 시
session.clear()후 재발급 - Fly에서 키가 없거나 예전 공개 기본값이면 그 값으로 서명하지 않음
- 쿠키
HttpOnly,SameSite=Lax, HTTPS면Secure, 수명 8시간 next파라미터는 외부 호스트·/admin밖 경로를 버림(오픈 리다이렉트 차단)- 로그인 POST에도 CSRF 토큰 요구
- IP당 실패 8회/10분 초과 시 429,
- 검증:
test_api2_login_locks_rotates_session_and_rejects_open_redirect,test_api2_fly_cookie_is_secure_and_secret_is_not_the_public_default. 조치 완료
API3. Broken Object Property Level Authorization
- 개념: 객체 속성 단위로 읽기·쓰기를 구분하지 않으면 과다 노출이나 대량 할당(Mass Assignment) 발생
- 위험성: 요청에 끼워 넣은 추가 속성이 클라이언트가 정하면 안 되는 값을 바꿀 수 있음
- 적용 전 상태: 위험
- 정렬 함수는
id,parent_id,sort_order만 읽어 이름·슬러그 변경은 불가했음 - 다만 알 수 없는 필드를 거부하지 않고 무시함. 폼도 동일
- 정렬 함수는
- 조치 내용: 필드 허용 목록 밖은 400 또는 저장 거부
- 정렬 JSON 최상위:
items,csrf_token - 정렬 항목:
id,parent_id,sort_order - 글 폼:
title,category_id,body_html,published,csrf_token - 카테고리 폼:
name,parent_id,csrf_token - 업로드: 파일
image와csrf_token
- 정렬 JSON 최상위:
- 검증:
test_api3_and_api5_reject_extra_fields_and_missing_csrf. 조치 후 양호
API4. Unrestricted Resource Consumption
- 개념: 요청 크기·횟수·연산에 상한이 없으면 자원을 무제한 소모함
- 위험성: 로그인·정렬·업로드 반복만으로 프로세스와 저장 공간이 묶일 수 있음
- 적용 전 상태: 취약
- 상한은
MAX_CONTENT_LENGTH10MB 하나뿐 - 로그인·정렬·업로드 속도 제한 없음
- 제목·카테고리 이름·본문·정렬 건수 서버 상한 없음
- 상한은
- 조치 내용
- 정렬 JSON 64KB 초과 413, 항목 500건 초과 400
- 관리 JSON·업로드 IP당 60회/분, 초과 429
- 제목 160자, 카테고리 이름 40자, 본문 1,000,000자
- 폼 비파일 필드 1MB, 파트 20개. 전체 요청 10MB는 유지
- gunicorn: 요청 줄 4094, 헤더 40개, 타임아웃 30초, 워커 1·스레드 2
- 공개 HTML GET은 제한하지 않음. 검색 로봇과 일반 방문 보호 목적
- 검증:
test_api4_limits_login_json_and_body_size. 조치 완료
API5. Broken Function Level Authorization
- 개념: 기능 단위 인가가 없으면 일반 요청이 관리 기능을 실행함
- 위험성: 관리 변경이 세션 플래그만으로 열리면, 로그인된 브라우저가 의도하지 않은 변경을 실행할 수 있음(CSRF)
- 적용 전 상태: 취약
@required데코레이터가 세션 플래그만 확인- CSRF는 카테고리 정렬에만 있음
- 삭제·게시·업로드·로그아웃·카테고리 변경을 타 사이트에서 호출 가능
- 조치 내용
admin_required가 계정 일치와 POST/PUT/PATCH/DELETE의 CSRF를 함께 확인- 비로그인 요청은 302로
/admin/login, 데이터 불변 - CSRF 실패 시 JSON·업로드는 400, 폼은 대시보드로 복귀
- 검증:
test_api3_and_api5_reject_extra_fields_and_missing_csrf,tests/test_categories.py정렬 인증 테스트. 조치 후 양호
API6. Unrestricted Access to Sensitive Business Flows
- 개념: 로그인·공개·삭제처럼 업무상 민감한 흐름은 자동화·남용 방지 후 개방해야 함
- 위험성: 잠금 없는 로그인, 세션만으로 되는 게시·삭제가 관리 상태를 바꿈
- 적용 전 상태: 취약
- 로그인 잠금 없음
- 게시·삭제가 다른 사이트의 폼 제출로 실행 가능
- 조치 내용
- API2 로그인 잠금 + API5 CSRF를 이 흐름에 적용
- 대시보드·편집기·로그인 폼이 토큰을 자동 포함해 기존 버튼 동작 유지
- 삭제 확인창
confirm을data-confirm과 페이지 스크립트로 옮겨 CSP와 양립 - 재인증 비밀번호 창은 미도입. 관리 단계를 늘리지 않기 위함
- 검증:
test_api6_publish_flow_requires_csrf_and_still_publishes. 조치 완료
API7. Server Side Request Forgery
- 개념: 서버가 사용자 제공 URL을 검증 없이 요청하면 SSRF임
- 위험성: 서버가 내부 주소나 클라우드 메타데이터 주소를 대신 열 수 있음
- 적용 전 상태: 양호
urlopen,requests,httpx호출 없음- 글 HTML의
a href,img src는 브라우저에서만 쓰임 - 서버가 글 안의 URL을 가져가지 않음
- 조치 내용: 향후 대비용 반출 허용 목록 추가
hardening.outbound_url_allowed는 Google Fonts 두 호스트만 허용- http, 사용자 정보 포함 URL, 메타데이터 주소, 기타 호스트 거부
require_outbound_url을 서버 반출의 유일한 통과점으로 지정
- 검증:
test_api7_outbound_allowlist_blocks_metadata_and_private_urls. 양호 유지
API8. Security Misconfiguration
- 개념: 응답 헤더, 디버그, 서버 표기, 배포 액션 고정이 빠지면 설정 오류임
- 위험성: 프레임 삽입, 콘텐츠 스니핑, 디버그 출력, 버전 노출, 고정되지 않은 액션이 공격면을 넓힘
- 적용 전 상태: 취약
- 관리 경로
X-Robots-Tag외 보안 헤더 없음 debug=True를 코드에서 강제로 끄지 않음- 개발 서버가
Werkzeug/<버전>표기 - 배포 액션
setup-flyctl@master
- 관리 경로
- 조치 내용
- CSP(스크립트는
'self'+요청별 nonce),nosniff,X-Frame-Options: DENY, Referrer·Permissions·COOP 정책 추가 - HTTPS에서 HSTS 1년+
includeSubDomains, CSPupgrade-insecure-requests - 관리 응답
Cache-Control: no-store,Referrer-Policy: no-referrer app.debug매 요청 거짓 고정. 500은 일반 문구만 반환- gunicorn
Server헤더 제거 - 업로드는 확장자와 이미지 시그니처가 둘 다 맞을 때만 저장
- PR #38: 글 8–13의 GitHub 이미지가 막혀
img-src에raw.githubusercontent.com한 곳만 추가
- CSP(스크립트는
img-src 'self' data: blob:
https://raw.githubusercontent.com
- 검증:
test_api8_security_headers_debug_and_image_signature, api-sec-check 보안 헤더 항목 충족. 조치 완료
API9. Improper Inventory Management
- 개념: 실제 떠 있는 경로와 문서·버전이 어긋나면 목록 밖 API(그림자 API)가 남음
- 위험성: 문서화되지 않은 관리 JSON이나 옛 화면이 호출 대상으로 남을 수 있음
- 적용 전 상태: 위험(낮음)
- 관리 JSON 1건이 목록으로 고정되어 있지 않음
/security,/admin/fly-billing템플릿은 남아 있으나 라우트가 없어 운영 404/api/v1같은 옛 버전 경로는 없음
- 조치 내용
- 라우트·메서드 집합을 테스트로 잠금. 새 경로가 생기면 CI 실패
- 버전 경로를 새로 열지 않음
/health는 버전 정보 없이{"status":"ok"}만 반환
- 검증:
test_api9_route_inventory_matches_the_blog. 조치 후 양호
API10. Unsafe Consumption of APIs
- 개념: 제3자 스크립트·글꼴·빌드 액션·패키지를 검증 없이 받으면 그 공급망을 그대로 실행함
- 위험성: SRI 없는 CDN 스크립트나 고정되지 않은 액션·의존성이 바뀌면 페이지와 배포가 함께 바뀜
- 적용 전 상태: 취약
- 홈·관리 화면이 jsDelivr의 Sortable 1.15.6, Quill 2.0.3, Three 0.170.0을 SRI 없이 로드
setup-flyctl이@masterrequirements.txt는 Flask와 gunicorn만 고정, 전이 의존성 미고정
- 조치 내용
- 세 라이브러리를 같은 버전으로
/static/vendor/에 두고 자체 제공 - CSP
script-src에서 CDN 호스트 제거 - 글꼴은 Google Fonts 두 호스트만 CSP 허용
actions/checkout,setup-python,setup-flyctl을 커밋 SHA로 고정- Flask 3.1.1, gunicorn 23.0.0과 전이 의존성
==고정
- 세 라이브러리를 같은 버전으로
- 검증:
test_api10_dependencies_and_workflows_are_pinned. 로컬 헤드리스 확인에서 홈 교량 장면scene-ready도달. 조치 완료
api-sec-check 사용법
- 저장소에 포함한 읽기 전용 점검 도구. 대상 URL의 응답 헤더·쿠키·CSRF 거부만 확인함
- 기본 실행은 로그인 요청을 보내지 않음
- 기대값은 위 조치 후 기준임
python -m pip install \
-e ".[test]"
api-sec-check \
https://xn--9h0bj97ao2i.com
python -m pytest
점검 항목
- 보안 헤더와 CSP, HTTPS일 때 HSTS·
upgrade-insecure-requests script-src에'self'와 nonce,'unsafe-inline'·'unsafe-eval'·외부 출처 없음img-src, 글꼴 호스트 범위/admin/login의no-store,no-referrer,noindex- 쿠키
blog_session의HttpOnly·SameSite=Lax·Secure Server헤더에 Werkzeug·gunicorn·버전 숫자 없음/health본문이{"status":"ok"}- CSRF 토큰 없는 로그아웃·삭제·정렬 POST가 거부되는지
--login-test지정 시에만 429·Retry-After: 60확인. 기본 9회, 상한 12회, 429 나오면 중단
종료 코드
- 0: 미충족 없음
- 1: 미충족 있음
- 2: 인자 오류
- 주의·건너뜀은 종료 코드를 올리지 않음. CI 게이트로 바로 사용 가능
운영 사이트 실행 결과
- 명령:
api-sec-check https://xn--9h0bj97ao2i.com(--login-test미사용) - 결과: 충족 24, 주의 2, 미충족 0, 건너뜀 1(로그인 429)
- 주의 1:
style-src의'unsafe-inline' - 주의 2: Fly 가장자리
Server: Fly/<빌드>헤더
보안 헤더·관리 응답·쿠키·버전 노출 항목. style-src와 Server 두 건만 주의임. (출처: 본인 깃허브)
/health, CSRF 거부(302 로그인 이동), 로그인 속도 제한 건너뜀과 요약 줄. (출처: 본인 깃허브)
JSON 출력 끝부분의 요약: 충족 24, 주의 2, 미충족 0, 건너뜀 1. (출처: 본인 깃허브)
잔여 리스크
- style-src 'unsafe-inline': 관리 편집기 Quill이 요소 스타일을 써서 남김. 스크립트 쪽에는
'unsafe-inline'·'unsafe-eval'없음 - Server: Fly 헤더: Fly 가장자리가 붙이는 빌드 표기. 애플리케이션 버전 노출은 아님
- 분산 대입: 로그인 잠금이 IP 기준이라 IP를 나눠 시도하는 대입은 남음
- 메모리 기반 제한: 잠금·속도 제한이 프로세스 메모리 기준. 머신이 늘면 버킷이 나뉨. 현재 머신 1·워커 1
- Google Fonts: 브라우저가 계속 요청함. 사용자 본문은 보내지 않음
- 미사용 템플릿:
/security등 라우트 없는 템플릿 파일은 삭제하지 않고 404 유지
결론
- 작은 개인 블로그도 관리자 라우트와 JSON 1개만으로 API Top 10 중 6개 항목이 취약 판정이었음
- 핵심 조치는 인증 강화, CSRF, 필드 허용 목록, 요청 상한, 보안 헤더, 공급망 고정임
- 조치는 방문자 화면과 관리 흐름을 바꾸지 않는 선에서 적용함
- 항목마다 테스트를 남겨 CI에서 회귀를 막고, api-sec-check로 운영 응답을 외부에서 재확인함
- 남은 주의 2건(
style-src, FlyServer)은 원인과 영향을 검토 문서에 기록해 둠
관련 글: OWASP Top 10 공격기법 및 대응방안