security: KVE-2026-2140 주문 보호와 5.6.39 보안 업데이트 배포

This commit is contained in:
whitedot committed 2026-09-14 06:53:14 +00:00
1 parent 2517343d51
commit f7aa440a80
43 files changed
+1510 -328

No files matched your search

+21
View File
@@ -2,6 +2,14 @@
데이터베이스 스키마 변경은 설정·목록·상세 화면이나 공통 라이브러리의 일반 실행 경로에서 수행하지 않는다. 신규 설치의 최종 스키마는 `install/gnuboard5.sql`에 반영하고, 기존 설치 변경은 `migrations` 디렉터리의 버전형 SQL 파일로 관리한다.
## 신규 설치 이력
기존 테이블이 없는 신규 설치는 최신 스키마와 기본 데이터를 생성한 뒤, `data/dbconfig.php`를 만들기 전에 배포본의 모든 마이그레이션 ID·설명·체크섬을 성공 이력으로 등록한다. 적용 시각은 설치 시각이고 실행시간은 0이다. 과거 마이그레이션 SQL은 실행하지 않으며, 쇼핑몰 설치 선택 여부와 관계없이 해당 배포본을 기준선으로 기록한다. 설치 직후 DB 업그레이드에서 별도의 기존 상태 확인 이력 등록이 필요하지 않다.
마이그레이션 디렉터리가 비어 있거나 파일 읽기 실패 또는 이력 저장 실패 시 설치 완료를 중단한다. 해당 접두어의 기존 테이블이 있거나 기존 DB를 유지·재설치하는 경로는 일괄 완료로 기록하지 않는다. 남아 있는 쇼핑몰·SMS 등의 구버전 테이블을 완료로 오인하지 않도록 DB 업그레이드에서 실제 상태를 확인한다.
`php tests/migration_install_history.php /path/to/isolated/mysql.sock`은 root/빈 비밀번호인 격리 MySQL에 임시 DB를 생성하여 사용자 지정 접두어, 쇼핑몰 선택 여부, 설치 이력과 체크섬, 재실행 방지, 이력 저장 실패 및 잘못된 이력 테이블의 1054 오류와 실행 차단을 검증한다. 검사 종료 시 임시 DB를 삭제한다. `python3 tests/migration_install_flow.py /path/to/isolated/mysql.sock`은 임시 설치 디렉터리에서 실제 설치 코드를 실행하여 일반·쇼핑몰 신규 설치, 기존 테이블 보존과 마이그레이션 파일이 없을 때 설정 파일 생성 차단을 검증한다.
## 파일 규칙
- 파일명은 `YYYYMMDD_NNN_설명.sql` 형식을 사용한다.
@@ -49,6 +57,8 @@
## 권한과 배포
`migrations/`는 업그레이드 완료 후에도 유지한다. Apache 2.4용 `migrations/.htaccess`를 함께 배포하며, Nginx는 별도의 서버 설정이 필요하다. 공유호스팅 적용 조건, 하위 경로 설치 예시와 개별 SQL 파일의 차단 확인 방법은 [마이그레이션 폴더 접근 차단](migration-directory-security.md)을 참고한다. HTTP 접근 차단 후에도 PHP의 서버 내부 파일 읽기는 허용해야 한다.
관리자 DB업그레이드 실행 시에는 DB 계정에 필요한 DDL 권한이 있어야 한다. 평상시 DDL 권한을 제거한 운영 환경에서는 업그레이드 시점에 필요한 권한을 부여하고, 완료 후 운영 권한으로 복원한다.
배포 전에는 백업과 테이블 크기, 예상 잠금 시간을 확인한다. MySQL 계열 DB의 DDL은 트랜잭션으로 완전히 되돌릴 수 없으므로 실패한 마이그레이션의 오류를 확인하고 수동 복구 후 재실행한다.
@@ -56,3 +66,14 @@
## 기존 업그레이드 코드
과거 설치본을 화면 접근 시점에 보정하던 누적 DDL은 일반 실행 파일에서 제거했다. `orderupgrade.php`와 `adm/dbupgrade.php`에 누적됐던 배포 후 변경은 실제 도입 시점별 마이그레이션으로 이전했다. 조사 근거와 대응 목록은 `database-migration-history.md`에 기록한다.
## 이력 테이블 구조 오류 복구
`Unknown column 'migration_id' in 'field list'`는 실행기가 사용하는 이력 테이블에 `migration_id` 컬럼이 없을 때 발생한다. 현재 설치 SQL에는 이 컬럼이 포함되어 있다. 기존의 `CREATE TABLE IF NOT EXISTS`는 이미 존재하는 테이블의 구조를 고치지 않으며, 과거 실행기는 이력 조회 실패를 빈 이력으로 취급하여 첫 이력 저장에서 오류가 드러날 수 있었다. 실제 발생 환경에서 구조가 달라진 경위는 테이블 정의와 설치 파일을 확인해야 한다.
조회와 실행 시 필수 컬럼 및 `migration_id` 단일 기본키를 확인한다. 구조 또는 이력 조회에 실패하면 오류를 표시하고 마이그레이션 실행과 이력 등록을 중단한다. 화면 조회에서는 테이블을 변경하지 않는다.
1. DB 백업 후 `SHOW CREATE TABLE g5_migrations`와 `SELECT COUNT(*) FROM g5_migrations`로 구조와 기존 이력을 확인한다. 사용자 지정 접두어를 사용했다면 실제 테이블명으로 바꾼다.
2. **잘못된 구조이며 이력이 없는 테이블**은 다른 용도로 사용되는 테이블이 아닌지 확인하고, 사용하지 않는 이름으로 보존한다. 예: `RENAME TABLE g5_migrations TO g5_migrations_backup_20260914`. 이름이 중복되지 않는지 먼저 확인한다.
3. 최신 파일을 배포한 뒤 DB 업그레이드의 전체 실행 또는 기존 상태 확인 이력 등록을 다시 수행한다. 정상 이력 테이블을 생성하고 실제 DB 상태에 따라 처리한다. 기존 상태 등록은 선행 실행 필요 항목에서 중단하므로 남은 작업은 전체 실행으로 처리한다.
4. **기존 이력이 있는 테이블**은 컬럼 매핑과 체크섬을 검토하여 현재 정의로 복구한다. 정상 성공 이력을 잃으면 조건 없는 데이터 보정이 재실행될 수 있으므로, 이력을 임의 삭제하거나 빈 테이블로 대체하지 않는다. `install/gnuboard5.sql` 전체를 기존 DB에 실행하면 테이블이 삭제되므로 복구용으로 실행하지 않는다.
+69
View File
@@ -0,0 +1,69 @@
# 마이그레이션 폴더 접근 차단
`migrations/`는 DB 업그레이드에 필요한 SQL 파일을 보관한다. 업그레이드 완료 후에도 적용 상태와 체크섬 확인, 후속 업데이트를 위해 폴더와 파일을 유지한다. HTTP 접근만 차단하고, 서버 내부에서 PHP가 파일을 읽을 수 있도록 한다.
## Apache 2.4 및 공유호스팅
배포본의 `migrations/.htaccess`에는 다음 설정이 포함된다.
```apache
Require all denied
```
FTP·파일 관리자로 업로드할 때 숨김 파일인 `.htaccess`도 포함해야 한다. 기존에 해당 파일을 수정했다면 설정을 병합한다. 이 규칙은 폴더와 개별 SQL 파일의 HTTP 접근을 차단하며 관리자 DB 업그레이드의 파일 읽기에는 영향을 주지 않는다.
호스팅에서 `.htaccess`와 인증·접근 제어 지시문을 허용해야 적용된다. `AllowOverride None`이면 무시되며, 지시문 사용이 제한된 환경에서는 HTTP 500이 발생할 수 있다. 차단 여부를 아래 방법으로 확인하고, 적용되지 않으면 호스팅 업체에 해당 폴더와 하위 파일의 HTTP 접근 차단을 요청한다. 웹서버 관리자는 `AllowOverride AuthConfig` 또는 필요한 지시문을 허용하는 설정을 검토할 수 있다. 자세한 조건은 [Apache 공식 안내](https://httpd.apache.org/docs/2.4/howto/htaccess.html)를 참고한다.
## Nginx
Nginx는 `.htaccess`를 적용하지 않는다. 사이트를 서비스하는 `server` 블록에 다음 설정을 추가한다. 도메인 루트에 그누보드를 설치한 경우의 예시다.
```nginx
location = /migrations {
return 404;
}
location ^~ /migrations/ {
return 404;
}
```
하위 경로에 설치했다면 실제 공개 URL에 맞춰 두 경로를 모두 바꾼다. 예를 들어 `/gnuboard5-dev/`에 설치한 경우 다음과 같다.
```nginx
location = /gnuboard5-dev/migrations {
return 404;
}
location ^~ /gnuboard5-dev/migrations/ {
return 404;
}
```
`^~`는 해당 접두어가 선택되었을 때 일반 정규식 location보다 우선하도록 한다. 기존의 더 구체적인 location, alias, 별도 호스트나 프록시 경로로 같은 파일을 제공한다면 해당 경로도 함께 차단한다. [Nginx location 공식 문서](https://nginx.org/en/docs/http/ngx_http_core_module.html#location)를 참고한다.
서버 관리자는 설정 파일을 저장한 뒤 문법 검사를 통과한 경우에만 반영한다.
```bash
sudo nginx -t
sudo systemctl reload nginx
```
공유호스팅에서 Nginx 설정 권한이 없다면 위 설치 경로와 설정 예시를 호스팅 업체에 전달한다. Apache 앞에서 Nginx가 정적 파일을 직접 제공하는 구성도 Nginx 측 차단이 필요하다. 이 작업을 위해 SQL 확장자나 파일명을 임의로 변경하지 않는다.
## 적용 확인
사이트 주소와 설치 경로를 바꿔 확인한다. 마지막 URL은 실제 존재하는 SQL 파일이어야 한다.
```bash
curl -i 'https://example.com/migrations'
curl -i 'https://example.com/migrations/'
curl -i 'https://example.com/migrations/20260910_001_kcp_notification.sql'
```
- 폴더와 개별 파일 요청 모두 HTTP 403 또는 404가 반환되고 SQL 본문이 노출되지 않아야 한다. 폴더 목록만 숨기는 `index.html`이나 디렉터리 목록 비활성화만으로는 개별 파일 다운로드를 막을 수 없다.
- 사이트 전체 인증으로 HTTP 401이 반환되는 것만으로 폴더 전용 차단을 확인할 수는 없다. 사이트 인증을 통과한 요청에서도 SQL 파일이 차단되는지 확인한다.
- HTTP 500은 정상 차단으로 간주하지 않는다. 호스팅의 허용 지시문과 오류 로그를 확인한다.
- 관리자 **환경설정 → DB업그레이드**에서 기존 마이그레이션 목록과 상태가 정상적으로 조회되는지 확인한다. 검증 목적으로 마이그레이션을 재실행할 필요는 없다.
PHP 실행 계정에는 가능한 한 읽기 권한만 부여하고 파일 변경은 배포 계정으로 관리한다. 두 계정이 같은 공유호스팅에서는 계정별 권한 분리가 어려울 수 있으므로 `chmod` 값만으로 변조 방지가 보장된다고 안내하지 않는다. 폴더를 쓰기 가능하게 만들거나 `777`로 설정하지 않는다. DB 백업·실제 비밀번호·개인정보가 포함된 파일은 이 폴더에 보관하지 않는다.
+35
View File
@@ -0,0 +1,35 @@
# 그누보드(영카트) 5.6.39 배포 안내
배포일: 2026-09-14. 변경 기준은 5.6.38입니다.
이번 버전은 임시 주문 정보 접근과 결제 복귀 보호를 강화하고, 마이그레이션 경로 보호와 설치·DB업그레이드를 보완한 보안 업데이트입니다. **파일 반영 후 최고관리자 DB업그레이드가 필요합니다.**
## 업데이트 절차
1. 신규 결제 접수를 잠시 중지하고 소스·설정·DB를 백업합니다. 진행 중인 결제와 PG에서 승인됐으나 주문이 완료되지 않은 거래를 먼저 대조합니다.
2. 5.6.38 설치본에 전체 소스 또는 `gnuboard5.6.39.patch.zip`을 반영합니다. 이번 패치의 삭제 대상 파일은 없습니다. 사용자 수정 파일은 비교·병합하고 `migrations/.htaccess`도 업로드합니다. 더 오래된 버전은 [5.6.38 안내](release-5.6.38.md)와 선행 버전의 설정·삭제 파일·DB 변경을 함께 확인하십시오.
3. 최고관리자로 **환경설정 → DB업그레이드**에서 선행 마이그레이션과 `20260914_001_order_access_state`를 실행합니다. 새 주문 상태 테이블은 InnoDB를 사용합니다. 주문 잠금과 장바구니 잠금은 별도 DB 연결을 사용하므로 일반 결제 요청당 최대 2개의 추가 연결을 고려하십시오. [DB업그레이드 안내](database-migrations.md)를 확인하십시오.
4. 사용자 주문 스킨·테마에 주문별 체크아웃 필드와 토큰 헤더 수신·결제 복귀 전달 변경을 반영하고 PHP·JavaScript 파일 및 캐시를 함께 갱신합니다. 공통 임시 저장에서 허용하지 않은 사용자 추가 필드는 저장되지 않으므로 [주문 보호 문서](security-kve-2026-2140.md)의 저장 허용 목록을 확인하십시오.
5. Apache는 `.htaccess` 적용 여부를 확인하고, Nginx는 별도의 접근 차단 설정을 반영합니다. `migrations/`와 실제 SQL 파일 URL 모두 403 또는 404가 반환되어야 합니다. 업그레이드 후에도 마이그레이션 파일을 유지하십시오. [서버별 설정 안내](migration-directory-security.md)를 확인하십시오.
6. 결제 복귀 URL은 HTTPS를 사용하고 웹 서버·프록시·APM에서 주문 상태 토큰을 기록하지 않도록 마스킹합니다. 과거 임시 주문은 PG 원장과 대조한 후 제공된 CLI 도구로 정리·복구합니다. 승인 여부가 불명확한 거래를 임의로 삭제하거나 다시 승인하지 마십시오.
7. 사용 중인 PG의 테스트 환경에서 PC·모바일, 회원·비회원, 결제 앱 복귀, 승인·취소·재시도, 비회원 주문 조회를 확인합니다. 주문·PG 금액과 상태가 일치하고 미완료 거래 처리 방침이 정해진 뒤 접수를 재개하십시오.
## 보안 변경
- **KVE-2026-2140:** PC·모바일 Toss와 모바일 KCP의 임시 주문 복원·승인·주문 저장에 주문별 복귀 토큰, 소유자·장바구니·PG·유효기간 검증을 적용합니다. 쿠키 없는 복귀와 다중 탭을 주문별로 처리하며, 토큰으로 일반 로그인 세션을 발급하지 않습니다.
- 승인 상태와 결과를 기록하여 중복 승인과 응답 유실 후의 잘못된 재처리를 방지합니다. Toss는 거래 조회·멱등 처리로 복구하며, KCP의 불명확 거래와 주문 일부 저장은 운영자의 원장 대조가 필요합니다.
- 공통 임시 주문 저장 항목을 허용 목록으로 제한하고 비회원 비밀번호·토큰을 복원용 데이터와 주문 POST 로그에서 제외합니다. 비밀번호는 서버 해시로 보호하며 완료된 임시 정보와 만료 데이터를 정리하는 CLI 도구를 제공합니다. 기존 데이터는 파일 교체만으로 모두 정리되지 않습니다.
- `migrations/.htaccess`와 Nginx 설정 안내를 제공하여 마이그레이션 SQL 파일의 HTTP 접근을 차단할 수 있도록 합니다.
## 설치·호환성 보완
- 신규 설치 완료 전에 해당 배포본의 마이그레이션 이력을 등록합니다. 이력 등록에 실패하면 설치 완료로 진행하지 않습니다.
- 기존 마이그레이션 이력 테이블의 구조 불일치와 조회 실패를 감지해 잘못된 업그레이드 실행을 차단합니다.
- PHP 5.2.17 구문 호환을 유지하도록 주문 보호 코드와 Toss 클래스 등을 보완합니다. 안전한 난수원·SHA-256·JSON이 필요하며, PG 연결에는 해당 PG가 요구하는 cURL/TLS 환경이 별도로 필요합니다.
- MySQL 5.0의 연결당 이름 잠금 제약을 고려하여 업무·주문·장바구니 잠금 연결을 분리합니다. 연결 실패 또는 잠금 유실 시 승인 상태 처리를 중단합니다.
## 검증 범위와 운영 확인
주문 접근·저장 10건, 토큰 전달 8건, PHP 5.2.17·7.4.33·8.4.22의 호환 함수 각 35건과 운영 PHP 33개 파일의 문법 검사, 안전한 난수원 부재 시 거부 검사를 통과했습니다. MySQL 8.0.45의 실제 연결 잠금 9건, 마이그레이션 이력·줄바꿈 13건, 신규 설치 4개 시나리오와 설치 스키마 대조를 확인했습니다. 격리된 PHP 8.4·MySQL·HTTP 환경에서 결제 상태 50건과 주문 확정 37건을 통과했으며, PG 통신은 대역을 사용했습니다.
실제 외부 PG 승인·취소·앱 복귀, MySQL 5.0 서버 자체 및 모든 사용자 스킨·운영 환경의 연동을 검증한 결과는 아닙니다. 다른 PG도 공통 저장 변경의 영향을 받으므로 정상 주문 확인이 필요합니다. 지원 버전의 구문 호환과 구형 TLS 환경에서의 실제 PG 연결 가능성은 구분해야 합니다. 상세 복구·정리 절차와 검증 한계는 [KVE-2026-2140 문서](security-kve-2026-2140.md)를 참고하십시오.
+110
View File
@@ -0,0 +1,110 @@
# KVE-2026-2140 주문 상태·복귀·복구
## 구현 범위
1차 세션 검사에 주문별 복귀 토큰, 영속 승인 기록, 데이터 정리와 운영 대조 도구를 추가했다. PC·모바일 Toss와 모바일 KCP의 반환·승인·주문 저장을 대상으로 한다. 공통 임시 저장의 필드 제한과 비밀번호 보호는 다른 PG에도 적용된다. 다른 PG의 쿠키 없는 복귀·승인 재시도를 이 구현이 대신 처리하지는 않는다.
- 임시 저장은 256비트 난수 토큰을 발급한다. DB에는 SHA-256 해시만 저장하고, 토큰 원문은 `X-G5-Order-State` 응답 헤더로 현재 탭에 전달한다.
- 토큰은 주문번호·PG·회원/비회원·장바구니·일반/개인결제·저장 본문 해시·만료 시각에 연결된다. 원문 임시 데이터와 비밀번호 해시를 읽기 전에 토큰과 최소 메타데이터를 검증한다.
- 새 테이블 `order_access`의 주문별 잠금과 최종 처리 시 장바구니별 잠금을 사용한다. 서로 다른 세션/프로세스에서도 승인부터 주문 저장까지 중복 처리를 막는다. 같은 장바구니의 다른 미완료 승인도 차단한다.
- 폼을 표시할 때 주문별 체크아웃 nonce와 장바구니를 세션에 등록한다. 다른 탭이 현재 주문 세션을 바꾸더라도 원래 탭은 자기 주문을 저장·복원한다. 같은 상품 장바구니를 두 번 결제하는 것은 허용하지 않는다.
- 쿠키 없는 복귀는 토큰으로 서버 상태를 복원한다. 회원 주문의 경우 해당 결제 요청 안에서만 원래 회원 컨텍스트를 적용하고 로그인 세션은 발급하지 않는다. 다른 회원으로 로그인한 브라우저의 토큰 사용은 거부한다. 결제 토큰은 해당 주문에 대한 bearer 권한이므로 로그·URL 보관에 주의한다.
- 최종 주문의 이름·주소·금액·포인트·쿠폰·배송비 등 업무 필드는 서버가 저장한 데이터로 복원한다. 결제 키·PG 암호문 등 승인 응답 필드는 별도로 검증한다. 정상 결제 금액은 기존 서버 장바구니 계산값 및 개인결제 금액과 대조한다.
- 공통 임시 저장은 `lib/shop_order_fields.lib.php`의 기본 주문서/PG별 허용 목록만 저장한다. 비밀번호와 토큰은 복원용 데이터에 보관하지 않는다. 사용자 스킨의 추가 업무 필드는 서버 허용 목록에 명시적으로 추가해야 한다.
- 비회원 비밀번호는 서버 해시로 분리 보관하며, 최종 주문에서 재해싱하지 않는다. 완료 후 임시 행·토큰·해시·응답 상태를 정리한다. 주문 POST 로그에서 비밀번호와 상태/체크아웃 토큰을 제외한다.
## 승인 상태와 복구
| 상태 | 처리 |
|---|---|
| `pending` | 승인 미시작. 기본 복귀 수명 2시간. `G5_ORDER_DATA_ACCESS_TTL`로 조정 가능 |
| `approving` | PG 요청 직전에 금액·요청 식별자를 영속 기록. 응답 유실 가능성이 있으므로 무조건 재승인하지 않음 |
| `approved` | 검증된 PG 결과를 저장. 후속 주문 저장을 계속할 수 있음 |
| `finalizing` | 주문 DB 반영을 시작함. 일부만 저장되었을 가능성이 있음 |
| `completed` | 주문 처리가 끝남. 토큰·비밀번호 해시·PG 응답은 지워짐 |
| `cancel_pending` | 취소 요청 시작. 결과 대조 전에는 승인 결과를 재사용하지 않음 |
| `cancelled`, `failed` | 확인된 종료 상태. 유예 후 정리 가능 |
| `legacy`, `unknown` | 운영자 대조 필요. 자동 삭제·재승인·토큰 발급 금지 |
| `purged` | 임시 개인정보 및 인증 상태 제거. 주문번호 재사용 방지용 기록만 유지 |
Toss는 안정적인 `Idempotency-Key`로 승인한다. 응답을 놓친 요청과 승인 후 중단된 요청은 PG 조회를 먼저 수행하고 주문번호·결제 키·금액·상태를 대조한다. 취소·실패한 거래의 저장된 성공 응답을 재사용하지 않는다. PG가 같은 거래의 `IN_PROGRESS` 상태를 확인한 경우에만 동일 키로 승인 재시도하며, 멱등 키 유효기간보다 짧은 14일로 제한한다. 조회 자체가 실패하면 재승인하지 않는다. 취소에도 동일 거래의 고정 멱등 키와 거래번호 대조를 적용한다.
KCP는 검증된 승인 결과를 별도 기록해 같은 요청 재처리 시 결과를 복원한다. 통신 도중 중단되어 승인 결과가 없는 경우에는 KCP 거래 원장을 확인한 운영자가 복구 자료를 제공해야 한다. 자동 조회 API를 임의로 가정하지 않았다.
일반 주문이 `finalizing`에서 중단됐으나 주문 행이 없고 원래 선택 장바구니가 남아 있으면 승인된 거래를 계속 처리한다. 주문 행이 이미 생겼거나 개인결제가 일부 반영된 경우에는 자동 덮어쓰기/재승인하지 않는다. 포인트·쿠폰·주문·장바구니를 운영자가 대조한 뒤 완료 처리한다. 기존 주문/장바구니 테이블이 MyISAM인 환경에서 전체 업무 트랜잭션을 원자적으로 되돌릴 수 있다고 가정하지 않는다.
## 설치·배포
신규 설치 SQL과 `migrations/20260914_001_order_access_state.sql`을 함께 제공한다. 기존 설치는 최고관리자 DB업그레이드에서 선행 마이그레이션을 처리한 후 이 마이그레이션을 적용한다. 일반 페이지에서는 DDL을 실행하지 않는다. 새 테이블은 InnoDB를 사용한다. MySQL 5.0의 연결당 이름 잠금 하나 제약에 맞춰 주문 잠금과 장바구니 잠금은 각각 별도 비영속 DB 연결을 사용한다. 기존 업무·포인트 DB 연결의 잠금과 분리되며, 연결 종료 시 서버가 잠금을 해제한다. 이 처리에는 일반 결제 요청당 최대 2개의 추가 DB 연결이 필요하다. 연결·잠금 획득 실패나 잠금 연결 유실이 확인되면 승인 상태 처리를 중단한다. 정리 도구는 항목별로 잠금을 해제하여 처리 건수만큼 연결이 누적되지 않는다.
배포 순서:
1. 신규 결제 접수를 일시 중단하고 DB 및 웹 공개 경로 밖의 미완료 거래 백업을 확보한다.
2. 미완료 주문·PG 승인 내역을 확인한다. 승인된 거래를 정리 대상으로 표시하지 않는다.
3. 마이그레이션과 PHP/JS 변경을 함께 적용한다. 사용자 스킨에도 체크아웃 필드, 응답 헤더 수신 및 복귀 토큰 전달 변경을 반영한다. 새 PHP에 예전 JS가 캐시되지 않도록 배포 캐시를 갱신한다.
4. 반환 URL의 HTTPS와 토큰 전달을 확인한다. Toss 성공/실패 URL과 KCP Ret_URL에 `g5_order_state`가 전달된다. KCP의 쿠키 재전송 우회는 토큰 형태를 인식하는 것뿐이며, 실제 인증은 엔드포인트의 DB 해시 검증으로 수행한다.
5. 웹서버·프록시·APM에서 결제 복귀 URL의 쿼리와 Referer를 로그에 기록하지 않거나 `g5_order_state`를 마스킹한다. 예를 들어 Nginx 결제 반환 로그는 `$request_uri`/`$request` 대신 쿼리 없는 `$uri`를 사용한다. 응답은 `no-store` 및 `Referrer-Policy: no-referrer`를 설정한다. 응답 헤더와 POST 본문을 별도 수집하는 서비스에도 같은 마스킹을 적용한다.
6. 계약된 PG 테스트 환경에서 외부 앱 복귀·성공·실패·취소·재통보를 확인한 후 접수를 재개한다. 코드 push와 운영 배포/PG 검증은 구분한다.
## 기존 요청과 정리 도구
`php tools/shop-order-maintenance.php`는 CLI 전용이며 기본값은 읽기 전용 목록이다. 웹에서는 실행되지 않는다. 목록과 오류에 토큰·비밀번호·전체 개인정보·PG 응답을 출력하지 않는다.
```sh
php tools/shop-order-maintenance.php --action=list
php tools/shop-order-maintenance.php --action=cleanup
php tools/shop-order-maintenance.php --action=cleanup --apply
php tools/shop-order-maintenance.php --action=reconcile-toss --order=주문번호 --apply
php tools/shop-order-maintenance.php --action=quarantine-legacy --order=주문번호 --apply
```
`cleanup`은 기본 24시간의 유예 후 완료·확인된 실패·취소 및 만료된 미승인 Toss/KCP 요청만 처리한다. 1회 최대 500건이다. `--grace=초`는 최소 1시간이다. `approving/approved/finalizing/unknown/cancel_pending`과 검토 전 과거 데이터는 자동 정리하지 않는다. 운영 환경에서 명시적으로 예약 실행할 수 있다. 일반 조회 요청에 정리 작업을 붙이지 않는다.
1차 패치의 주문·세션·본문 해시 검증을 통과하는 과거 요청은 새 상태로 전환할 수 있다. 그보다 오래된 요청은 주문번호 또는 PAYREQ_MAP만으로 신뢰하거나 토큰을 재발급하지 않는다. `quarantine-legacy`는 지정 주문의 평문 비밀번호를 제거하고 해시를 별도 보관하되 접근 불가능한 `legacy` 상태로 격리한다. 승인된 미완료 거래를 먼저 PG/주문 자료와 대조해야 한다.
KCP의 불명확한 승인 결과는 원장을 확인한 뒤 다음 명령으로 반영한다. JSON에는 `orderId`, `amount`, `tno`, `res_cd`와 해당 결제수단의 `app_time`, `app_no`, 은행/가상계좌 등의 실제 승인 결과를 넣는다. 자료는 웹 공개 경로 밖에 보관한다. 이 명령은 운영자의 원장 확인을 명시적으로 신뢰하며, 파일 자체를 PG 서명으로 간주하지 않는다.
```sh
php tools/shop-order-maintenance.php --action=reconcile-kcp --order=주문번호 --receipt=/private/verified-kcp.json --verified --apply
php tools/shop-order-maintenance.php --action=complete-reviewed --order=주문번호 --verified --apply
php tools/shop-order-maintenance.php --action=purge-reviewed-legacy --order=주문번호 --receipt=/private/legacy-review.json --verified --apply
```
`complete-reviewed`는 `finalizing` 거래의 주문·PG·장바구니·포인트·쿠폰 대조를 마친 경우에만 사용한다. 저장된 거래번호도 승인 기록과 비교한다. 기존 데이터 삭제 자료는 `orderId`, `resolution`(`unpaid/cancelled/completed`), `reference`(원장 대조 기록)를 포함한다. `completed`를 선택하면 실제 저장된 주문/개인결제의 거래번호가 있어야 한다. 이 도구는 불명확한 거래를 자동으로 미승인으로 판정하지 않는다.
## 검증 방법과 한계
- `php tests/shop_order_access_test.php`: 1차 세션 상태의 안전한 전환 경계와 저장 허용 목록.
- `node tests/shop_order_state_browser_test.js`: 토큰 헤더·폼 전달·콜백 URL 인코딩. DOM 대역이며 실제 PG SDK 실행은 아니다.
- `tests/shop_order_state_test.py`: 실제 PHP/DB/HTTP, 쿠키 없는 Toss/KCP 복귀, 위조 토큰, 주문/금액/키 변조, 다중 탭, 병렬 프로세스 승인, 응답 유실·부분 저장·취소 상태, 정리와 과거 요청, 신규/기존/쇼핑몰 미설치 마이그레이션.
- `tests/shop_order_finalize_test.py`: 별도 테스트 설치본에서 PG 전송 클래스만 대체하고 실제 PC·모바일 주문 확정, 개인결제, 비회원 주문 조회, 완료 정리, 중복 POST를 실행한다. 테스트가 끝나면 원래 전송 클래스를 복원한다.
Python 검증에는 `G5_ORDER_TEST_CONFIG`로 별도 설치본의 JSON(`root`, `php`, `ini`, `port`)을 지정한다. 해당 설치본은 loopback DB 주소와 `issue48_` 접두어의 데이터베이스를 사용해야 하며 `data/.order-state-test`에 `KVE-2026-2140-local-test`를 적어 명시적으로 표시한다. fixture PHP는 운영 DB에서 실행을 거부한다. 테스트 파일은 Git에는 보관하지만 일반 배포 아카이브에서 제외한다.
실제 Toss/KCP 네트워크 승인·취소·재통보, PG 앱/브라우저별 쿠키 정책, 다른 PHP/DB 버전, 모든 사용자 스킨의 호환성은 이 로컬 대역 검증으로 확정하지 않는다. KCP 불명확 거래와 부분 주문 저장은 위 운영자 대조 경로로 처리하며 무조건 자동 복구한다고 안내하지 않는다.
Toss API 근거: [결제 API](https://docs.tosspayments.com/reference), [멱등성과 결제 후처리](https://docs.tosspayments.com/guides/v2/get-started/llms-quick-reference).
## 최소 버전 호환 처리
PHP 최소 버전은 기존 PHP 5.2.17을 유지한다. 공통 주문 라이브러리의 익명 함수와 `__DIR__`, PHP 7 전용 난수·역직렬화 옵션 의존을 제거했다. PC·모바일 주문서의 익명 콜백과 PHP 5.4 이후 JSON 출력 옵션, Toss 클래스의 스칼라·반환·프로퍼티 타입 선언도 구버전에서 읽을 수 있게 변경했다. 운영 도구는 구버전 HTTP 상태 헤더·JSON 출력·예외 처리를 사용한다.
`lib/shop_order_compat.lib.php`에서 다음을 공통 처리한다.
- 난수는 `random_bytes`, 강한 OpenSSL 난수, `/dev/urandom`, `MCRYPT_DEV_URANDOM` 순으로 사용 가능한 안전한 소스를 찾는다. 약한 난수로 대체하지 않으며 안전한 소스가 없으면 결제를 진행하지 않는다.
- 토큰·해시는 길이와 자료형을 확인하고 같은 길이의 문자열은 끝까지 비교한다.
- 기존 Base64/serialize 임시 데이터는 배열·문자열·정수·유한 실수·불리언·null만 읽는 전용 파서로 복원한다. 객체·참조·잘못된 길이·과도한 크기/깊이는 거부하며 PHP 객체 생성이나 `unserialize()`를 호출하지 않는다. 원문 길이와 바이너리 문자열을 유지하므로 기존 주문 필드와 KCP 응답을 읽을 수 있다.
인증 토큰 생성과 주문 상태 처리에는 hash(SHA-256)·JSON 및 안전한 난수원이 필요하다. PG 통신에는 해당 PG가 요구하는 cURL/TLS 환경이 별도로 필요하며, PHP 구문 호환이 구형 TLS 라이브러리로 실제 PG 연결까지 가능함을 보장하지 않는다. 사용자 주문 스킨의 토큰 전달 반영 요건은 그대로 유지한다.
검증 명령:
```sh
php tests/shop_order_compat_test.php
php -d disable_functions=random_bytes,openssl_random_pseudo_bytes,mcrypt_create_iv tests/shop_order_compat_test.php --no-rng
php tests/shop_order_lock_test.php 127.0.0.1:포트 issue48_테스트DB
```
설계 근거: [MySQL의 GET_LOCK 버전별 동작](https://dev.mysql.com/blog-archive/making-get_lock-behavior-more-predictable-cross-version-with-query-rewrite/)과 [mysql_connect의 new_link 옵션](https://www.php.net/manual/en/function.mysql-connect.php)을 기준으로 잠금 연결을 분리했다. PHP의 [random_bytes 지원 범위](https://www.php.net/manual/en/function.random-bytes.php)와 [PHP 7의 역직렬화 필터 도입](https://www.php.net/manual/en/migration70.new-features.php)을 확인하고 구버전용 처리를 추가했다. DB 파일 잠금은 여러 웹서버 간 보호를 보장하지 않으므로 사용하지 않는다.
호환 함수와 구형 mysql 드라이버의 단일 잠금 동작 대역은 PHP 5.2.17·7.4·8.4에서 검사한다. 실제 잠금 검사는 root/빈 비밀번호인 격리 MySQL에서 업무·주문·장바구니 연결 분리, 다른 프로세스의 경합, 종료·연결 단절 시 해제와 유실 감지를 확인한다. MySQL 5.0 서버 자체의 실행 검증과 실제 PG 통신은 이 검사에 포함하지 않는다. 기존 주문 상태·최종 주문 회귀 검증도 별도 설치본에서 실행한다.