플러그인 없이 1회차 · Claude Code · Opus 5.5 · 생각 깊이 중간 · 클로드 답변(보고서) · 목록

정적 분석으로 충분히 확인되는 취약점들이라 서버 실행(PoC 구동)은 생략하고, 코드 근거를 짚어 보고서로 정리했습니다. 코드는 수정하지 않았습니다.


달빛카페 예약 관리 — 보안 점검 보고서

대상: app/ (server.py, views.py, db.py, config.py) · Python 표준 라이브러리 기반 http.server 점검일: 2026-10-18 요약: 심각(Critical) 3건, 높음(High) 2건, 중간(Medium) 4건, 낮음/참고 다수. 인증 없이 DB 전체를 읽을 수 있는 SQL 인젝션과, 서버 임의 파일을 내려받는 경로 탈출이 가장 위험합니다.


🔴 심각 (Critical)

C-1. SQL 인젝션 — 이름 검색 (/search)

위치: app/db.py:66-70, 진입점 app/server.py:45-47

f"SELECT id, guest_name, visit_time, people FROM reservations WHERE guest_name LIKE '%{name}%'"

사용자 입력 name을 f-string으로 직접 쿼리에 끼워 넣습니다. /search는 인증도 필요 없습니다.

조치 방향: 파라미터 바인딩 사용 — WHERE guest_name LIKE ? 에 ('%'+name+'%',) 전달. (대조적으로 list_today의 정렬 컬럼은 SORT_COLUMNS 화이트리스트로 안전하게 처리되어 있습니다 — 같은 방식을 검색에도 적용해야 합니다.)

C-2. 경로 탈출(Path Traversal) — 영수증 다운로드 (/receipt)

위치: app/server.py:49-57

path = os.path.join(config.RECEIPT_DIR, q.get("file", ""))
with open(path, "rb") as f: ...

file 값을 검증 없이 os.path.join에 넘깁니다. os.path.join은 절대경로나 ..을 막지 못합니다.

로그인은 필요하지만, 아무 손님 계정(C-1으로 탈취한 토큰 포함)이면 됩니다.

조치 방향: 파일명만 허용(os.path.basename), 정규화 후 RECEIPT_DIR 하위인지 os.path.realpath로 재확인.

C-3. 저장형 XSS — 관리자 화면의 요청사항(note)

위치: app/views.py:17-25 (특히 22행), 입력 app/server.py:80

f"...<td>{int(r['people'])}</td><td>{r['note']}</td></tr>"

다른 필드는 모두 html.escape()로 처리했지만 note만 이스케이프가 빠져 있습니다. note는 손님이 예약 시 자유롭게 입력하는 값(/reserve의 note)입니다.

조치 방향: html.escape(r['note']).


🟠 높음 (High)

H-1. 관리자 비밀번호를 GET 쿼리로 전달 + 소스 하드코딩

위치: app/server.py:59-60, app/config.py:10

if q.get("pw") != config.ADMIN_PASSWORD:   # /admin?pw=moonlight2026!
ADMIN_PASSWORD = "moonlight2026!"

조치 방향: 자격증명은 환경변수/시크릿으로, 전달은 POST 바디 또는 세션 기반으로. 비교는 hmac.compare_digest.

H-2. 접근제어 미흡(IDOR) — 남의 예약 열람

위치: app/server.py:37-43, app/db.py:61-63

r = db.get_reservation(int(q.get("id", "0")))   # 소유자 확인 없음

로그인만 되어 있으면 id를 1씩 증가시켜 다른 손님의 예약 전체(이름·방문시간·인원·요청사항)를 순회 열람할 수 있습니다. get_reservation이 요청자의 guest_id와 대조하지 않습니다.

조치 방향: WHERE id=? AND guest_id=? 로 소유자 검증.


🟡 중간 (Medium)

M-1. 약한 비밀번호 해시 — MD5, 솔트 없음

위치: app/db.py:28-29

return hashlib.md5(pw.encode()).hexdigest()

MD5는 빠르고 충돌에 취약하며, 솔트가 없어 레인보우 테이블·대량 크래킹에 노출됩니다. C-1/C-2로 해시가 유출되면 대부분 즉시 복원됩니다.

조치 방향: bcrypt/scrypt/argon2 등 솔트 포함 느린 해시.

M-2. 로그인 무차별 대입 방어 부재 + 사용자 열거

위치: app/db.py:39-44, app/server.py:72-74

로그인 시도 횟수 제한·지연·락아웃이 없고, 해시 비교(==)가 비상수시간입니다. MD5(M-1)와 결합되면 오프라인·온라인 공격이 모두 쉬워집니다.

조치 방향: 레이트리밋/락아웃, hmac.compare_digest.

M-3. 세션 토큰 수명·회전 없음

위치: app/db.py:32-49

토큰이 발급 후 만료·폐기·재발급되지 않습니다. 한 번 유출되면(C-1, 로그 등) 영구 유효합니다. 로그아웃 기능도 없습니다.

조치 방향: 만료시간·서버측 폐기(로그아웃)·필요 시 회전.

M-4. 미처리 예외로 인한 500/정보노출·안정성

위치: app/server.py:40,79(int(...)), app/server.py:69,71,73(self._form()의 f["phone"] 등), app/db.py:35

핸들러에 전역 예외 처리가 없어 가용성·정보노출 위험이 있습니다.

조치 방향: 입력 검증·try/except, 공통 에러 핸들러.


🟢 낮음 / 참고


우선순위 권고

  1. C-1(SQLi)·C-2(경로탈출) — 인증 없이/최소 권한으로 DB·파일 전체가 털리므로 최우선 수정.
  2. C-3·H-1 — 관리자 계정 탈취로 이어지는 연쇄(저장형 XSS → URL 속 관리자 PW).
  3. H-2(IDOR)·M-1(MD5) — 개인정보 대량 열람·크래킹 방지.
  4. 나머지 M/L 항목.

필요하시면 각 항목의 수정 패치(диff)도 만들어 드리겠습니다 — 다만 "코드는 고치지 말라"는 지시에 따라 지금은 적용하지 않았습니다.