퐁퐁마켓은 화이트햇 스쿨 소프트웨어 개발 실습·시큐어 코딩 과제를 위해 만든 Django 기반 중고거래 플랫폼입니다. 회원, 상품, 실시간 전체채팅과 DM, 신고와 자동 차단, 원화형 테스트 지갑, 카카오페이 테스트 충전, 관리자 관리 기능을 하나의 로컬 실행 환경에서 확인할 수 있습니다.
이 README는 저장소를 처음 내려받은 사용자가 다른 문서를 먼저 읽지 않아도 다음 사항을 판단하고 재현할 수 있도록 작성했습니다.
- 무엇이 구현되어 있고 무엇이 구현되어 있지 않은지
- 일반 Python 실행과 Docker HTTPS/WSS 실행의 차이
- 필요한 프로그램, 환경변수, Secret key와 콜백 설정
- 일반 사용자·정지 사용자·staff·superuser의 권한 차이
- 기능별 사용 방법과 주요 URL
- 자동 테스트, 수동 점검, 데이터 백업·초기화 방법
- 현재 보안 통제와 공개 운영 전 남아 있는 한계
Phase 0부터 Phase 11까지의 구현과 보안 보완은 모두 main에 통합되어 있습니다. 실행과 기능 평가의 유일한 기준 브랜치는 main이며, 최종 제출 시점은 v1.0.0-submission 태그로 고정합니다.
최종 통합 검증에서 다음 결과를 확인했습니다.
| 구분 | 확인 결과 |
|---|---|
| Django 호스트 테스트 | 142/142 통과 |
| Docker 컨테이너 테스트 | 142/142 통과 |
| 일반 실행 | Python 가상환경, Django 개발 서버, SQLite |
| Docker 실행 | Caddy HTTPS/WSS, Daphne ASGI, SQLite named volume |
| 실제 브라우저 흐름 | 회원가입·상품·전체채팅·DM·신고·지갑·관리자 통과 |
| 카카오페이 | 테스트 CID TC0ONETIME ready·QR·approve 확인 |
| 보안 점검 | Django check, Bandit, pip-audit, Gitleaks, Grype, ZAP 및 수동 검증 |
| 저장소 공개 상태 | 2026-07-24 확인 기준 PUBLIC |
카카오페이 검증은 실제 현금이 결제되는 운영 결제가 아닙니다. 개발용 Secret key와 테스트 CID TC0ONETIME을 사용하는 카카오페이 테스트 흐름입니다. 운영 CID, 현금 출금, 자동 환불, 사용자 간 실제 정산은 구현하지 않았습니다.
- Django 인증 기반 회원가입, 로그인, 로그아웃, 비밀번호 변경
- 닉네임과 소개글을 관리하는 마이페이지
- 로그인 username과 공개 닉네임·
public_id분리 - 닉네임 기반 사용자 검색과 공개 프로필
- 일반 로그인과 관리자 로그인 분리
- 정지 사용자에 대한 주요 행동 제한
- 로그인·관리자 로그인 반복 실패 제한
비밀번호와 지갑 2차 비밀번호는 평문으로 저장하지 않습니다. 공개 화면과 URL에는 순차 사용자 ID나 로그인 username 대신 UUID 기반 public_id와 닉네임을 사용합니다. UUID는 권한검사를 대신하지 않으므로 서버에서도 소유자·참여자 권한을 별도로 확인합니다.
- 상품 등록, 전체 목록, 상세, 검색, 수정, 삭제
- 판매자만 자기 상품을 수정·삭제할 수 있는 서버 측 소유권 검사
- 전자기기, 가구/인테리어, 의류, 도서/음반, 생활/주방, 스포츠/레저, 기타 카테고리
- 등록된 카테고리 값을 기준으로 한 실제 필터링
- 판매중·판매완료 상태
- JPG, JPEG, PNG, WEBP 이미지 업로드
- 업로드 이미지 디코드·재인코딩을 통한 메타데이터와 후행 바이트 제거
- 차단 상품의 공개 목록·검색 제외
이미지는 최대 5MB, 최대 1,200만 픽셀입니다. 서버는 확장자만 믿지 않고 Pillow로 이미지를 해석하고 허용 형식으로 다시 저장합니다. 상품 설명은 최대 2,000자이며 가격은 음수가 될 수 없는 정수 원 단위입니다.
- Django Channels와 WebSocket 기반 전체 실시간 채팅
- 두 참여자만 접근할 수 있는 1:1 다이렉트 메시지
- 상품 상세의 CSRF 보호 POST 버튼으로 판매자와 DM 시작
- 새 DM 수신 알림과 읽지 않은 메시지 수
- 모든 화면 우측 하단의 플로팅 메신저에서 전체채팅·DM 전환
/products/에서 상품 목록과 플로팅 전체채팅을 동시에 사용- 전송한 메시지의 데이터베이스 저장
- 허용 Origin, 로그인 세션, 정지 상태의 연결·수신 시점 재검사
채팅 메시지는 최대 1,000자입니다. 기본 발송 제한은 대화 대상별 5초 동안 5개, 60초 동안 30개입니다. 현재 제한 저장소와 channel layer는 메모리 기반이므로 한 Daphne 프로세스 또는 한 개발 서버에서 사용하는 로컬 과제 환경에 적합합니다.
- 상품 신고와 사용자 신고
- 신고 사유 최대 1,000자 저장
- 자기 자신 또는 자기 상품 신고 거부
- 같은 신고자의 같은 대상 중복 신고 거부
- 서로 다른 유효 신고자 3명 이상이면 상품 자동 차단
- 서로 다른 유효 신고자 3명 이상이면 사용자 자동 정지
- 신고 접수·상품 차단·사용자 정지의 별도 moderation 감사 기록
- Django Admin에서 신고와 감사 기록 검토
정지 사용자는 상품 등록·수정, 신고, 채팅, 송금을 할 수 없습니다. 정지 전에 등록한 자기 상품을 삭제하는 것은 허용합니다. 단순 임계치 방식이므로 공개 운영 서비스에 필요한 이의제기와 수동 승인 절차까지 제공하는 것은 아닙니다.
- 신규 사용자별 원화형 테스트 지갑
- 가입 시 테스트용 기본 잔액 10,000원
- 카카오페이 결제수단과 6자리 2차 비밀번호를 설정하는 최초 지갑 설정
- 닉네임·공개 UUID를 사용하는 사용자 간 내부 테스트 송금
- 송금 내역과 카카오페이 충전 내역
- 행 잠금과 데이터베이스 transaction을 사용한 원자적 잔액 변경
- 카카오페이 테스트 ready·approve 후 정확히 한 번만 잔액 증가
- 불확실하거나 불일치한 외부 승인 결과의
REVIEW_REQUIRED격리
| 제한 | 값 |
|---|---|
| 신규 지갑 기본 잔액 | 10,000원 |
| 지갑 최대 잔액 | 1,000,000,000원 |
| 한 번의 내부 송금 최대값 | 1,000,000,000원 |
| 카카오페이 최소 충전 | 1,000원 |
| 카카오페이 최대 충전 | 1,000,000원 |
송금액은 1원 이상이어야 하고 잔액을 초과할 수 없으며, 자기 자신에게 송금할 수 없습니다. 송신자와 수신자 모두 활성 사용자여야 합니다. 카카오페이 충전은 브라우저가 성공 URL로 돌아왔다는 이유만으로 반영하지 않고 서버가 제공자 승인 응답의 TID, 주문, 사용자, 금액, AID를 확인한 뒤 한 번만 반영합니다.
- 별도 관리자 로그인 페이지
- 실제 데이터로 집계되는 관리자 대시보드
- 가입자·상품 등록 추이와 신고 유형 분포
- 사용자, 프로필, 상품, 신고, 채팅, 지갑, 거래, 카카오페이 충전 관리
- 대시보드에서 실제 Django Admin 관리 목록으로 이동
- 일반 사용자 페이지를 관리자 계정으로 점검 가능
- 기본
/admin/과/admin/login/에 대한 404 응답
관리자 URL을 다르게 두는 것은 노출을 줄이는 보조 수단일 뿐 접근통제의 대체물이 아닙니다. 서버는 항상 is_staff, is_superuser, Django Admin 모델 권한을 검사합니다.
| 기능 | 비로그인 | 일반 회원 | 정지 회원 | staff | superuser |
|---|---|---|---|---|---|
| 홈·공개 상품 조회 | 가능 | 가능 | 가능 | 가능 | 가능 |
| 회원가입·일반 로그인 | 가능 | 가능 | 계정 상태에 따라 제한 | 관리자 계정 로그인 거부 | 관리자 계정 로그인 거부 |
| 상품 등록·수정 | 불가 | 본인 상품만 | 불가 | 일반 사용자 기능 점검 가능 | 일반 사용자 기능 점검 가능 |
| 자기 상품 삭제 | 불가 | 가능 | 가능 | 권한에 따라 가능 | 가능 |
| 신고·채팅·송금 | 불가 | 가능 | 불가 | 일반 사용자 정책과 Admin 권한 적용 | 가능 |
| Django Admin | 불가 | 불가 | 불가 | 부여된 모델 권한만 | 전체 |
| 통합 관리자 대시보드 | 불가 | 불가 | 불가 | 불가 | 가능 |
staff와 superuser는 일반 로그인 페이지가 아니라 관리자 전용 로그인 페이지를 사용합니다. 로그인 후 일반 사용자 화면도 열어 기능을 확인할 수 있지만, Django Admin의 실제 관리 범위는 부여된 권한에 따라 결정됩니다.
브라우저
├─ HTTP 요청 ───────────────┐
└─ WebSocket 연결 ──────────┤
▼
Django 개발 서버 / ASGI
│
SQLite + local media
일반 실행은 설정과 기능 확인이 가장 간단합니다. DEBUG=1이며 기본 주소는 http://127.0.0.1:8000입니다. 데이터는 구현 폴더의 db.sqlite3와 media/에 저장됩니다.
브라우저
│ HTTPS / WSS
▼
Caddy proxy : 127.0.0.1:80, 127.0.0.1:443
│ 내부 Docker network의 HTTP / WebSocket
▼
Daphne + Django Channels : app:8000
│
├─ SQLite named volume
├─ media/static named volume
└─ 읽기 전용 runtime secret files
Docker 경로는 공개 운영 배포가 아니라 localhost에서 HTTPS/WSS와 컨테이너 경계를 재현하는 선택형 실습 환경입니다. 호스트에는 Caddy의 80/443만 127.0.0.1로 공개하고 Daphne 8000은 Docker 내부 network에서만 사용합니다.
| 앱 | 핵심 모델 | 책임 |
|---|---|---|
accounts |
Profile |
공개 UUID, 닉네임, 소개, 정지 상태 |
products |
Product |
판매자, 상품 정보, 카테고리, 이미지, 공개·판매 상태 |
reports |
Report, ModerationAuditLog |
신고 대상·사유, 자동 차단·정지 감사 기록 |
chat |
GlobalChatMessage, PrivateChatRoom, PrivateChatMessage |
전체채팅, 두 참여자 DM, 읽음 시각 |
wallet |
Wallet, PointTransaction, KakaoPayCharge |
잔액, 내부 송금 원장, 외부 테스트 충전 상태 |
사용자 삭제와 연결 데이터의 관계는 Django model의 CASCADE 또는 PROTECT 정책을 따릅니다. 카카오페이 충전 기록은 원장 추적을 위해 참조 사용자가 존재하는 동안 보호하고, 일반 상품·채팅·신고 테스트 데이터는 관련 사용자를 삭제할 때 정해진 cascade 정책으로 함께 정리됩니다. Django Admin에서 지갑 잔액이나 외부 승인 상태를 임의 수정하는 것은 정상 운영 절차로 사용하지 않습니다.
| 방향 | 대상 | 목적 |
|---|---|---|
| 브라우저 → Django/Caddy | HTTP(S) | HTML, 폼, 이미지, 관리자 |
| 브라우저 → Channels | WS 또는 WSS | 전체채팅과 DM |
Django → open-api.kakaopay.com |
HTTPS | 테스트 결제 ready·approve |
| 브라우저 → KakaoPay | HTTPS redirect | 테스트 QR·사용자 승인 |
| 브라우저 → Google Fonts·jsDelivr | HTTPS | 웹폰트, Material Symbols, Bootstrap CSS |
외부 인터넷이 차단되어도 서버와 핵심 Django 기능은 실행할 수 있지만 CDN 웹폰트·아이콘·Bootstrap 일부 표현은 대체 글꼴이나 단순한 모양으로 보일 수 있습니다. 카카오페이 충전은 외부 네트워크와 개발자 설정이 없으면 사용할 수 없습니다.
secure-coding/
├── AGENTS.md
├── README.md
├── README_CODEX_SETUP.md
├── docs/ 설계·테스트·보안·보고서
└── tiny-secondhand-platform/
├── manage.py
├── accounts/
├── products/
├── reports/
├── chat/
├── wallet/
├── config/
├── templates/
├── static/
├── deploy/
├── Dockerfile
├── compose.yaml
├── requirements.lock
└── .env.example
모든 Django 명령과 Docker 명령은 tiny-secondhand-platform/에서 실행합니다. Markdown 문서는 저장소 최상위 또는 docs/에만 둡니다.
- Python 3.12
- Django 5.2.16
- Django Channels 4.3.2
- Daphne 4.2.2
- Pillow 12.3.0
- SQLite
- Django Templates와 Bootstrap
- Docker Compose
- Caddy 2.11.4
정확한 Python 의존성과 해시는 tiny-secondhand-platform/requirements.lock에 고정되어 있습니다.
- Git
- Python 3.12
- Python venv와 pip
- WSL2 Ubuntu 24.04 또는 동등한 Linux 환경
- 카카오페이 충전까지 확인하려면 카카오페이 개발자 애플리케이션과 개발용 Secret key
Ubuntu에서 python3 -m venv가 동작하지 않으면 python3-venv 패키지가 필요합니다.
- Docker Desktop
- Docker Compose v2
- Docker Desktop의 WSL integration
- 호스트의
127.0.0.1:80과127.0.0.1:443을 사용할 수 있는 상태
이미 다른 프로그램이 80 또는 443 포트를 사용하면 Compose proxy가 시작되지 않습니다.
git clone https://github.com/aoistar-kr/secure-coding.git
cd secure-coding/tiny-secondhand-platform
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install --require-hashes -r requirements.lock저장소가 아직 PRIVATE이면 GitHub 접근 권한이 있는 계정으로 인증해야 clone할 수 있습니다.
cp .env.example .env
chmod 600 .env카카오페이 충전까지 포함한 전체 기능을 확인하려면 .env를 다음과 같이 설정합니다.
테스트에 사용할 카카오페이 개발용 Secret key 값은 저장소와 별도로 제출되는 보고서의 맨 마지막 페이지에 기재되어 있습니다. 해당 값을 복사해 아래 KAKAOPAY_SECRET_KEY= 오른쪽에 입력합니다. Secret key가 포함된 보고서는 비공개 제출용이므로 공개 GitHub 저장소에 추가하거나 다른 사람에게 공유하면 안 됩니다.
DJANGO_SECRET_KEY=
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=https://localhost
KAKAOPAY_MODE=live
KAKAOPAY_CID=TC0ONETIME
KAKAOPAY_SECRET_KEY=여기에_별도로_전달받은_개발용_Secret_key를_붙여넣기
KAKAOPAY_PUBLIC_BASE_URL=http://127.0.0.1:8000실제 입력 방법은 다음과 같습니다.
nano .env.env에서 아래 줄을 찾습니다.
KAKAOPAY_SECRET_KEY=등호 오른쪽에 별도 제출 보고서의 마지막 페이지에 기재된 카카오페이 개발용 Secret key 값을 그대로 붙여 넣습니다. 등호 앞뒤에 공백을 넣거나 <, > 같은 안내용 문자를 함께 입력하면 안 됩니다.
KAKAOPAY_SECRET_KEY=실제로_전달받은_개발용_키_값저장 후 일반 실행에서는 다음 명령으로 .env를 현재 셸에 적용합니다.
set -a
source .env
set +a키 값을 화면에 출력하지 않고 설정 여부만 확인하려면 다음 명령을 사용합니다.
python -c 'import os; print("카카오페이 키 설정됨" if os.getenv("KAKAOPAY_SECRET_KEY") else "카카오페이 키 미설정")'카카오페이 키 설정됨이 출력되면 python manage.py runserver로 일반 실행을 시작할 수 있습니다. Docker도 같은 .env를 사용하므로 값을 입력한 뒤 docker compose up -d를 실행하면 됩니다. 실제 키 값을 확인한다는 이유로 echo, printenv, 스크린샷을 사용하지 마십시오.
일반 DEBUG=1 실행은 DJANGO_SECRET_KEY가 비어 있어도 임시 키를 생성합니다. 서버 재시작 후에도 로그인 세션을 유지하려면 다음 명령으로 값을 만들어 .env에 넣습니다.
python -c 'import secrets; print(secrets.token_urlsafe(64))'카카오페이 개발키가 없고 회원·상품·채팅·신고·내부 송금만 확인하려면 .env에서 다음과 같이 충전을 끌 수 있습니다.
KAKAOPAY_MODE=disabled
KAKAOPAY_SECRET_KEY=KAKAOPAY_MODE=live인데 개발키가 없으면 DEBUG=1 서버의 다른 기능은 열 수 있지만 충전 요청은 설정 오류로 실패합니다. Docker는 DEBUG=0으로 시작하므로 live 모드에서 키가 없으면 안전하게 시작을 거부합니다.
Django는 임의의 .env 파일을 자동으로 읽지 않으므로 일반 실행에서는 같은 셸에 직접 export해야 합니다.
set -a
source .env
set +a
python manage.py migrate
python manage.py check
python manage.py runserver브라우저에서 http://127.0.0.1:8000/에 접속합니다. KAKAOPAY_PUBLIC_BASE_URL을 http://127.0.0.1:8000으로 설정했다면 실제 접속 주소도 같은 origin을 사용하는 것이 안전합니다.
서버와 별도의 터미널에서 같은 가상환경과 환경변수를 적용한 뒤 실행합니다.
python manage.py createsuperuser생성한 계정은 일반 로그인 화면이 아니라 다음 관리자 로그인에서 사용합니다.
http://127.0.0.1:8000/secure-pongpong-admin/login/
| 변수 | 일반 실행 기본·권장값 | Docker 동작 | 설명 |
|---|---|---|---|
DJANGO_DEBUG |
기본 1 |
Compose가 0으로 고정 |
개발 오류 표시 여부 |
DJANGO_SECRET_KEY |
DEBUG에서는 비어 있어도 임시 생성 | 필수 | 세션·서명용 비밀키 |
DJANGO_ALLOWED_HOSTS |
localhost,127.0.0.1 |
같은 .env 값을 사용 |
허용 Host 목록 |
DJANGO_CSRF_TRUSTED_ORIGINS |
필요 origin 목록 | 기본 https://localhost |
HTTPS CSRF 신뢰 origin |
DJANGO_SESSION_COOKIE_AGE |
기본 28800 |
동일 | 세션 수명, 300~86400초 |
DJANGO_DB_PATH |
기본 db.sqlite3 |
/data/db/db.sqlite3 고정 |
SQLite 파일 경로 |
DJANGO_MEDIA_ROOT |
기본 media/ |
/data/media 고정 |
업로드 저장 경로 |
DJANGO_STATIC_ROOT |
기본 staticfiles/ |
/data/static 고정 |
collectstatic 결과 |
SECURITY_MAX_FAILURES |
기본 5 |
동일 | 로그인·PIN 실패 제한 횟수 |
SECURITY_LOCKOUT_SECONDS |
기본 300 |
동일 | 실패 제한 유지 시간 |
CHAT_RATE_LIMIT_BURST_MESSAGES |
기본 5 |
동일 | 짧은 구간 메시지 수 |
CHAT_RATE_LIMIT_BURST_SECONDS |
기본 5 |
동일 | 짧은 제한 구간 |
CHAT_RATE_LIMIT_SUSTAINED_MESSAGES |
기본 30 |
동일 | 긴 구간 메시지 수 |
CHAT_RATE_LIMIT_SUSTAINED_SECONDS |
기본 60 |
동일 | 긴 제한 구간 |
KAKAOPAY_MODE |
live 또는 disabled |
live 고정 |
외부 테스트 충전 활성화 |
KAKAOPAY_CID |
TC0ONETIME |
운영 CID 거부 | 테스트 결제 CID |
KAKAOPAY_SECRET_KEY |
전체 기능 사용 시 필수 | live 시작 시 필수 | 개발자 애플리케이션 Secret key |
KAKAOPAY_PUBLIC_BASE_URL |
http://127.0.0.1:8000 |
https://localhost 고정 |
제공자 콜백 기준 origin |
KAKAOPAY_HTTP_TIMEOUT_SECONDS |
기본 10 |
동일 | 외부 API timeout, 1~10초 |
Docker의 HTTPS redirect, Secure cookie와 HSTS 관련 값은 compose.yaml에 로컬 검증용 값으로 명시되어 있습니다. 공개 운영 환경에 그대로 복사하지 말고 실제 도메인과 인증서 정책에 맞게 별도 검토해야 합니다.
카카오페이 개발자 웹 플랫폼에는 실제 사용할 origin을 등록합니다.
일반 실행:
http://127.0.0.1:8000
브라우저에서 localhost를 실제로 사용할 경우 추가:
http://localhost:8000
Docker:
https://localhost
등록하는 값은 origin이며 UUID가 포함된 개별 callback 경로가 아닙니다. 애플리케이션이 각 결제 준비 요청에서 다음 형태의 동적 URL을 전달합니다.
/wallet/charge/kakaopay/<charge_public_id>/approve/
/wallet/charge/kakaopay/<charge_public_id>/cancel/
/wallet/charge/kakaopay/<charge_public_id>/fail/
KAKAOPAY_PUBLIC_BASE_URL은 브라우저에서 사용하는 등록 origin과 정확히 일치해야 합니다. Secret key, TID, pg_token, 세션 쿠키는 README, Git, 스크린샷, 브라우저 JavaScript에 넣으면 안 됩니다.
cd secure-coding/tiny-secondhand-platform
cp .env.example .env
chmod 600 .env
python -c 'import secrets; print(secrets.token_urlsafe(64))'출력값을 .env의 DJANGO_SECRET_KEY에 넣고 카카오페이 개발용 Secret key도 입력합니다. Compose는 KAKAOPAY_MODE=live를 사용하므로 전체 스택을 그대로 시작하려면 카카오페이 키가 필요합니다.
docker compose config --quiet
docker compose build --pull
docker compose up -d
docker compose ps정상 상태에서는 secret-init가 성공 후 종료되고, app과 proxy가 healthy가 됩니다. 앱은 시작할 때 migration과 collectstatic을 실행한 뒤 Daphne을 시작합니다.
docker compose logs --tail=100 app proxy브라우저 주소:
https://localhost/
Caddy는 공개 인증기관이 아니라 프로젝트 로컬 CA로 localhost 인증서를 만듭니다. 신뢰 등록 전 브라우저 인증서 경고는 정상입니다.
공개 루트 인증서를 무시 경로로 복사합니다.
mkdir -p .local-certs
docker compose cp proxy:/data/caddy/pki/authorities/local/root.crt .local-certs/root.crtWindows 현재 사용자 인증서 저장소에 선택적으로 등록합니다.
certutil -user -addstore Root "\\wsl.localhost\Ubuntu-24.04\home\<WSL사용자>\<저장소경로>\secure-coding\tiny-secondhand-platform\.local-certs\root.crt"현재 실행한 Compose proxy에서 복사한 인증서인지 확인한 뒤 등록해야 합니다. 검증 후에는 certmgr.msc의 현재 사용자 → 신뢰할 수 있는 루트 인증 기관에서 해당 Caddy Local Authority 지문을 확인하고 제거할 수 있습니다. Caddy 개인 CA 키가 들어 있는 Docker volume은 공유하면 안 됩니다.
secret-init만 무시된.env를 Compose secret으로 읽습니다.secret-init는 Django와 KakaoPay 값을 0440 파일로 나누고 종료합니다.- 앱은 비밀파일을 읽기 전용으로 마운트하며 실제 키를 이미지나 앱 환경에 넣지 않습니다.
- 앱은 UID/GID 10001, read-only root, 모든 capability 제거 상태로 실행됩니다.
- proxy는 read-only root에서 필요한
NET_BIND_SERVICEcapability만 가집니다. - SQLite 디렉터리와 파일은 시작 시 각각 0700과 0600으로 제한합니다.
- 앱 8000 포트는 호스트에 publish하지 않습니다.
- 앱·proxy·init에는 PID와 메모리 제한, health check가 적용됩니다.
이 구성은 로컬 과제의 방어 수준을 높이기 위한 것이며 외부 Secret Manager, 운영 DB 역할, 중앙 로그, 다중 인스턴스 가용성을 제공하는 것은 아닙니다.
docker compose down이 명령은 컨테이너와 network를 내리지만 named volume 데이터는 보존합니다.
docker compose down -v-v는 SQLite, 업로드 이미지, 정적 파일, runtime secret volume과 로컬 CA를 삭제하는 파괴적 초기화입니다. 필요한 DB·media 백업과 대상 Compose project 이름을 확인한 경우에만 사용합니다.
아래 URL은 일반 실행 기준입니다. Docker에서는 host를 https://localhost로 바꿉니다.
| URL | 로그인 | 용도 |
|---|---|---|
/ |
선택 | 홈, 카테고리, 최근 상품 |
/accounts/signup/ |
불필요 | 회원가입 |
/accounts/login/ |
불필요 | 일반 회원 로그인 |
/accounts/mypage/ |
필요 | 내 프로필과 활동 |
/accounts/profile/edit/ |
필요 | 닉네임·소개 수정 |
/accounts/password/change/ |
필요 | 비밀번호 변경 |
/accounts/users/ |
필요 | 닉네임 기반 사용자 검색 |
/accounts/users/<public_id>/ |
필요 | 공개 프로필 |
/products/ |
선택 | 전체 상품·카테고리와 플로팅 채팅 |
/products/search/ |
선택 | 상품명·카테고리 검색 |
/products/new/ |
필요 | 상품 등록 |
/products/<product_public_id>/ |
선택 | 상품 상세 |
/products/<product_public_id>/edit/ |
판매자 | 상품 수정 |
/products/<product_public_id>/delete/ |
판매자 | 상품 삭제 |
/reports/product/<product_public_id>/ |
필요 | 상품 신고 |
/reports/user/<public_id>/ |
필요 | 사용자 신고 |
/chat/global/ |
필요 | 전체채팅 확장 화면 |
/chat/ |
필요 | DM 방 목록 |
/chat/private/start/<public_id>/ |
필요·POST | DM 시작 |
/chat/private/rooms/<room_public_id>/ |
참여자 | DM 대화 |
/wallet/ |
필요 | 잔액·송금·충전 내역 |
/wallet/setup/ |
필요 | 카카오페이·6자리 PIN 설정 |
/wallet/transfer/ |
필요 | 내부 테스트 송금 |
/wallet/charge/kakaopay/ |
필요 | 카카오페이 테스트 충전 |
/secure-pongpong-admin/login/ |
관리자 | 관리자 로그인 |
/secure-pongpong-admin/ |
superuser | 통합 대시보드 |
/secure-pongpong-admin/django/ |
staff 권한 | Django Admin |
/healthz/ |
불필요 | 최소 health check |
로그아웃과 DM 시작 등 상태를 바꾸는 작업은 CSRF 보호 POST 요청으로 처리합니다.
WebSocket endpoint:
/ws/chat/global/
/ws/chat/private/<room_public_id>/
브라우저 코드가 현재 페이지의 HTTP/HTTPS에 맞춰 WS/WSS를 선택하므로 사용자가 주소를 직접 열 필요는 없습니다.
- 첫 번째 일반 계정으로 회원가입합니다.
- 마이페이지에서 닉네임과 소개를 수정합니다.
- 지갑을 열고 카카오페이 결제수단과 6자리 2차 비밀번호를 설정합니다.
- 카테고리와 유효한 이미지를 선택해 상품을 등록합니다.
- 로그아웃하고 두 번째 일반 계정을 만듭니다.
/products/에서 해당 카테고리로 필터링해 상품을 찾습니다.- 상품 상세에서 판매자에게 DM을 시작하고 메시지를 보냅니다.
- 플로팅 메신저에서 전체채팅을 전송하고 DM 알림을 확인합니다.
- 상품 또는 사용자를 한 번 신고해 신고 건수가 반영되는지 확인합니다.
- 두 계정 모두 지갑 설정 후 1원 이상의 테스트 금액을 송금합니다.
- 카카오페이 키가 있다면 1,000원 이상 테스트 충전을 진행하고 승인 후 잔액이 한 번만 증가하는지 확인합니다.
자동 차단·정지는 서로 다른 신고자 세 명이 필요합니다. 검증 계정을 만들 때 비밀번호나 PIN을 문서·스크린샷에 남기지 말고 검증 후 생성한 데이터와 이미지를 삭제합니다.
python manage.py createsuperuser로 관리자 계정을 만듭니다./secure-pongpong-admin/login/에서 로그인합니다.- 대시보드의 사용자·상품·신고·채팅·지갑 카드를 확인합니다.
- 각 바로가기를 눌러 실제 Django Admin 목록으로 이동합니다.
- 신고 대상, 차단 상태, moderation 감사 기록을 확인합니다.
- 카카오페이 충전 상태와 마스킹된 제공자 식별자를 확인합니다.
- 일반 상품 페이지로 이동해 관리자 계정으로 사용자 화면을 점검합니다.
일반 로그인 /accounts/login/은 staff와 superuser 인증을 거부하도록 의도적으로 분리되어 있습니다.
cd secure-coding/tiny-secondhand-platform
source .venv/bin/activate
python manage.py check
python manage.py makemigrations --check --dry-run
python manage.py test --verbosity 1전체 테스트는 현재 142개입니다. 카카오페이 테스트는 provider-shaped test double을 사용하므로 이 명령이 실제 외부 결제를 만들지 않습니다.
Django 배포 경고를 별도로 확인하려면 카카오페이를 비활성화한 임시 환경에서 실행할 수 있습니다.
DJANGO_DEBUG=0 \
DJANGO_SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(64))')" \
DJANGO_ALLOWED_HOSTS="market.example.com" \
KAKAOPAY_MODE=disabled \
python manage.py check --deploylive 카카오페이를 포함한 non-DEBUG 설정을 검사하려면 실제 개발키와 경로 없는 HTTPS KAKAOPAY_PUBLIC_BASE_URL도 필요합니다.
docker compose exec -T app python manage.py check
docker compose exec -T -e DJANGO_SETTINGS_MODULE=config.settings_test app \
python manage.py test --verbosity 1config.settings_test는 실행 중인 서비스의 HTTPS 설정을 약화하지 않고 Django 테스트 client만 정상적으로 동작하게 하는 운영 파생 테스트 설정입니다.
자동 테스트만으로 다음을 증명하지는 않습니다.
- 실제 브라우저에서 CSS·반응형 화면이 정상인지
- trusted localhost 인증서에서 HTTPS/WSS가 동작하는지
- 두 브라우저 세션 사이 DM과 새 메시지 알림이 보이는지
- 카카오페이 개발자 플랫폼 origin과 Secret key가 올바른지
- 관리자 대시보드 집계와 Django Admin 링크가 실제 데이터에 맞는지
| 데이터 | 기본 위치 |
|---|---|
| SQLite | tiny-secondhand-platform/db.sqlite3 |
| 업로드 이미지 | tiny-secondhand-platform/media/ |
| collectstatic 결과 | tiny-secondhand-platform/staticfiles/ |
| 비밀 환경파일 | tiny-secondhand-platform/.env |
백업할 때는 서버를 먼저 중지하고 DB와 media를 같은 시점에 복사합니다. .env는 소스 백업이나 제출물에 포함하지 말고 별도 비밀 저장소에서 관리합니다.
새 로컬 DB로 다시 시작하려면 기존 DB와 media를 즉시 삭제하기보다 다른 이름의 백업 위치로 옮긴 뒤 python manage.py migrate를 실행하는 편이 안전합니다. 새 DB에는 superuser가 없으므로 다시 생성해야 합니다.
Compose는 다음 named volume을 사용합니다.
runtime_secretssqlite_datamedia_datastatic_datacaddy_datacaddy_config
확인:
docker volume ls --filter label=com.docker.compose.project=pongpong-market백업은 앱 쓰기를 중지한 상태에서 SQLite와 media volume을 같은 시점으로 보존해야 합니다. 단순히 docker compose down한 뒤 다시 시작하면 데이터가 유지됩니다. docker compose down -v는 백업 없이 사용하면 안 됩니다.
일반 실행의 db.sqlite3와 Docker의 sqlite_data는 서로 다른 데이터셋입니다. 한쪽에서 만든 계정이나 상품이 다른 실행 방식에 자동으로 나타나지 않습니다.
- Django 기본 인증·비밀번호 해시·CSRF middleware 사용
- Django ORM 사용, 애플리케이션 raw SQL 없음
- 상품 소유권과 DM 참여자 서버 측 검사
- 공개 UUID와 로그인 username 분리
- 신고 중복·자기 신고 방지와 정지 사용자 정책
- 지갑 transaction·행 잠금·DB 제약·최대값 검증
- 카카오페이 고정 API origin, 테스트 CID 강제, 중복 승인 방지
- 업로드 크기·픽셀·형식 검사와 이미지 재인코딩
- WebSocket Origin·세션·사용자 상태 재검사
- 로그인·PIN·채팅 rate limit
- 로그인된 응답의
Cache-Control: private, no-store - 요청별 nonce 기반 CSP, X-Frame-Options, nosniff, Permissions-Policy
- 운영 모드의 HTTPS redirect, Secure cookie, HSTS
- 안전한 400·403·404·429·500 화면
- Docker 비루트 실행, read-only root, 제한된 capability와 내부 app port
- 해시 고정 Python 의존성, 비밀파일과 로컬 DB의 Git 제외
세션은 기본 8시간이며 브라우저 종료 시 만료되고 요청마다 만료 시점이 갱신됩니다.
README만으로 현재 환경을 평가할 때 다음 한계를 반드시 함께 봐야 합니다.
- 일반
runserver는 개발용이며 공개 운영 서버가 아닙니다. - Docker도 localhost 실습 환경이며 공개 도메인·공인 인증서를 제공하지 않습니다.
- SQLite는 DB 사용자 역할 분리, 네트워크 접근통제, 고가용성을 제공하지 않습니다.
InMemoryChannelLayer와 메모리 rate limit은 여러 app 프로세스가 공유하지 못합니다.- 관리자 MFA와 중요 작업 재인증은 구현하지 않았습니다.
- 중앙 로그 수집·경보, 외부 Secret Manager, 자동 백업·복구 훈련은 없습니다.
- CSP의 script에는
unsafe-inline을 허용하지 않지만 기존 동적 표현 때문에style-src-attr 'unsafe-inline'은 남아 있습니다. - 사용자 검색은 닉네임, 상품 검색은 제목과 저장된 카테고리 기준이며 가격 범위·고급 정렬·pagination은 없습니다.
- 이메일 인증·비밀번호 재설정 메일, 다중 상품 이미지, 즐겨찾기, 주문·배송 workflow는 과제 범위에 포함되지 않습니다.
- 채팅은 기본 전송·저장·DM 알림을 제공하지만 메시지 수정·삭제·첨부파일·운영자 실시간 모니터링은 없습니다.
- 외부 CDN에 접속하지 못하면 웹폰트·아이콘·Bootstrap 표현이 일부 달라질 수 있습니다.
- 신고 임계치 정책은 단순하며 이의제기·오탐 복구 workflow가 없습니다.
- 카카오페이 자동 환불, 주문 조회 기반 reconciliation, 출금, 현금 정산, chargeback은 없습니다.
- Docker 이미지 스캔에는 upstream에서 수정 버전을 제공하지 않은 잔여 항목이 있으므로 “취약점 0개”라고 주장하지 않습니다.
- 저장소가 private인 동안에는 권한 없는 평가자가 clone할 수 없습니다.
따라서 현재 구현은 “교수자가 로컬로 내려받아 모든 과제 기능과 보안 통제를 재현하는 환경”에는 적합하지만, 인터넷에 공개해 실제 돈과 개인정보를 처리하는 운영 서비스 수준은 아닙니다.
| 문서 | 확인할 내용 |
|---|---|
AGENTS.md |
개발 규칙, 승인 범위, 완료 기준 |
docs/ORIGINAL_REQUIREMENTS.md |
강의 슬라이드 원본 요구사항 |
docs/PROJECT_SPEC.md |
프로젝트 기능 명세 |
docs/REQUIREMENTS_TRACEABILITY.md |
요구사항·구현·검증 연결 |
docs/ARCHITECTURE.md |
앱, 모델, trust boundary |
docs/DEVELOPMENT_LOG.md |
Phase 0~11 의사결정과 검증 기록 |
docs/TEST_CHECKLIST.md |
자동·브라우저 점검 항목 |
docs/SECURITY_REVIEW.md |
누적 보안 검토 |
docs/PHASE11_SECURITY_ASSESSMENT.md |
최종 공격표면·도구·잔여 위험 |
docs/KAKAOPAY_INTEGRATION.md |
카카오페이 상태·콜백·보안 설계 |
docs/MAINTENANCE.md |
유지보수·백업·향후 개선 |
docs/FINAL_REPORT.md |
Markdown 최종 보고서 |
스캐너 원본과 브라우저 캡처는 저장소에서 무시되는 output/ 아래에 보관될 수 있습니다. 결과 수치와 해석은 문서에 남기되 Secret key, 비밀번호, PIN, 세션, CSRF token, provider token, private CA key는 증거에 포함하지 않습니다.
현재 GitHub 저장소는 Public이며 main이 유일한 실행·평가 기준입니다. Phase 1~11 작업 브랜치는 모두 main에 병합된 뒤 삭제했고, 개발 과정은 main의 커밋 기록과 docs/DEVELOPMENT_LOG.md에 보존합니다.
최종 제출 상태:
branch: main
tag: v1.0.0-submission
이후 수정이 필요한 경우 main에서 직접 작업하지 않고 다음처럼 짧고 목적이 드러나는 브랜치를 만듭니다.
codex/fix-간단한제목
codex/docs-간단한제목
변경을 검증해 main에 병합한 뒤 작업 브랜치는 삭제합니다. 현재 제출 이후 추가 수정은 예정되어 있지 않습니다.
공개 전 최종 확인에서는 다음을 완료했습니다.
- 변경사항을
main에 커밋하고 원격 저장소에 푸시했습니다. - GitHub repository visibility를 Public으로 변경했습니다.
- 인증 없는 요청에서 저장소 페이지와 Raw README의 HTTP 200 응답을 확인했습니다.
- 현재 Git 이력과 제출 변경분에서 Gitleaks 비밀정보 0건을 확인했습니다.
.env, SQLite, 업로드 media, 인증서 개인키, 비공개 보고서와 로컬 증거를 Git에서 제외했습니다.- 일반 실행과 Docker 실행, 142개 테스트, 실제 브라우저 통합 검증 결과를 문서와 일치시켰습니다.
저장소 주소:
https://github.com/aoistar-kr/secure-coding