Files
Gnuboard5/docs/security-kve-2026-2346.md

10 KiB

KVE-2026-2346 KCP 통보 검증 보강

KCP 공통 통보의 테스트 모드 발신 검증 누락으로 미결제 주문을 입금 처리할 수 있는 경로를 차단한다. shop/settle_kcp_common.php에서 요청을 검증한 뒤 lib/kcp_notification.lib.php가 주문·개인결제·장바구니와 처리 이력을 반영한다.

적용 절차

  1. 소스와 DB를 백업하고 주문·결제 접수를 잠시 중지한다. 변경 파일 반영 후 최고관리자의 환경설정 → DB업그레이드에서 20260910_001_kcp_notification까지 실행한다. 신규 설치 SQL에도 같은 스키마를 포함했다. 일반 요청에서는 DDL을 실행하지 않는다. 업그레이드 전에는 새 주문 저장 및 KCP 통보가 실패할 수 있다.
  2. DB 계정의 LOCK TABLES, 관련 테이블의 SELECT·INSERT·UPDATE 권한을 확인한다. 테스트 환경은 운영 주문과 분리한다.
  3. 기존 KCP 미결제 가상계좌 주문과 개인결제를 KCP 거래 내역에 대조한다. 새 od_kcp_site_cd·pp_kcp_site_cd는 기존 행에 빈값으로 추가되며, 빈 상점코드인 거래의 통보는 실패 응답한다. 현재 설정을 전체 과거 거래에 일괄 복사하지 않는다. 거래번호·주문번호·결제수단·청구액·계좌·테스트 여부·실제 상점코드를 확인한 건만 관리자가 별도 보정하고 KCP 재통보를 요청한다. 이미 입금된 기존 거래에는 새 이력을 임의로 만들지 말고 PG 기록과 대조하여 수동 정산한다.
  4. PC·모바일에서 실제 KCP 테스트 가상계좌 발급과 모의 입금을 확인한 뒤 접수를 재개한다. 기존 연동의 통보 발신 IP 및 처리구분이 아래 공식 안내와 다른 경우 KCP에 해당 연동의 값을 확인한다. 검증을 통째로 생략하거나 임의 전달 헤더로 우회하지 않는다.
  5. 실패 통보와 shop_kcp_noti의 미완료 이력을 점검하고 재통보 제한을 넘긴 건은 KCP 내역과 대조해 복구한다.

입력과 거래 검증

  • 테스트는 210.122.176.144, 운영은 103.215.144.173, 103.215.144.174, 210.122.72.173의 POST만 받는다. 과거 코드의 다른 IP는 자동 승계하지 않는다. 기준은 KCP 웹훅 가이드, 확인일은 2026-09-10이다. 이 목록이 구형 계약의 모든 발신지를 포함하는지 KCP에 별도 확인한 상태는 아니다.
  • REMOTE_ADDR만 사용한다. 리버스 프록시 환경은 신뢰하는 프록시와 원본 IP 복원 설정을 웹 서버에서 제한해야 한다. 요청 헤더·상점코드·관리자 로그인 자체를 발신 인증으로 취급하지 않는다.
  • 필수값은 문자열 형식과 길이를 검사하고 배열·빈 거래번호·잘못된 날짜·0원·음수·소수·지수 표기·DB 정수 범위 초과를 거부한다. 가상계좌 입금은 TX00, op_cd=50, 망취소는 op_cd=13으로 구분한다. 다른 처리구분은 실패 응답한다.
  • PC·모바일 주문과 개인결제 저장 시 서버의 $g_conf_site_cd를 보존한다. 테스트 T0000·T0007, 운영 SR 접두어를 포함한 실제 사용값을 비교하므로 이후 상점 설정 변경으로 과거 거래를 다른 상점에 연결하지 않는다.
  • 저장된 PG·상점·거래번호·가상계좌 결제수단·계좌를 모두 대조한다. 연결 주문은 존재해야 하고 테스트 환경도 일치해야 한다. 개인결제와 일반 주문의 번호가 충돌하면 실패한다.
  • 일반 입금은 서버 주문 합계에서 쿠폰·포인트 등을 반영한 청구액 전액과 일치하고, 입금액 0·주문 상태여야 한다. 개인결제는 pp_price와 대조하며 연결 주문 미수금을 초과할 수 없다. 연결 주문의 원래 PG·결제수단은 개인결제와 다를 수 있으므로 개인결제 자체의 KCP 거래를 검사한다. 연결 주문이 없는 독립 개인결제도 처리한다.
  • 기존에 처리하지 않던 TX01~TX07 이벤트는 상점·거래를 확인하고 수신 확인만 한다. 환불·에스크로 상태 자동화 기능을 새로 추가한 것은 아니다.

중복과 중간 실패

이력 키는 상점·거래·주문·통보 ID·입금/망취소 구분을 포함한다. 같은 통보 ID의 입금과 망취소가 각각 기록되며, 이미 완료한 동일 통보는 주문이 완료 상태가 되었어도 성공 응답한다. 같은 통보의 금액·계좌가 바뀌면 거부한다. 활성 입금이 있을 때 다른 통보 ID로 다시 입금할 수 없고, 망취소 후 지연 도착한 입금 통보도 금액을 다시 올리지 않는다.

MyISAM과 InnoDB 모두 관련 네 테이블의 쓰기 잠금을 잡고 검증·반영한다. 변경 전 값과 목표값을 kn_plan에 먼저 기록하고, 개인결제 금액과 연결 주문 입금액을 목표값으로 저장하여 재시도에서 중복 가산하지 않는다. 주문·장바구니·개인결제 반영과 결과 재조회가 끝난 뒤 kn_done=1을 기록해야 result=0000을 응답한다. 입력 불일치·스키마/권한 누락·SQL 오류·미완료 복구 충돌은 9999로 응답한다.

MyISAM의 여러 테이블 쓰기가 원자적 트랜잭션이 되는 것은 아니다. 중간 실패 시 일부 변경이 남을 수 있으며 같은 통보의 재시도로 완료한다. 물리적 서버 장애·디스크 손상에 대한 롤백도 보장하지 않는다. 재시도 시 현재 값이 기록된 변경 전/후 값과 다르면 외부 변경으로 판단하여 덮어쓰지 않는다. 해당 주문의 미완료 통보가 있으면 다른 신규 통보도 막는다. 테이블 전체 잠금을 사용하므로 통보 처리 중 주문·관리자 쿼리가 잠시 대기할 수 있다.

망취소는 해당 입금액만 차감하고 미수금을 복원한다. 주문/장바구니의 입금은 주문으로 되돌린다. 이미 준비·배송·완료인 주문은 금액과 미수금을 수정하되 물류 상태를 되돌리지 않는다. 이런 건은 출고 중지·회수·콘텐츠 권한·지급 포인트 등을 운영자가 별도로 대조해야 한다. 이미 취소·반품된 주문 또는 이력 없이 입금액이 남아 있는 기존 거래는 자동 차감하지 않고 실패로 남긴다.

복구 점검 예시(실제 설치의 테이블 접두어로 바꾼다):

SELECT kn_key, kn_trade, kn_noti_id, kn_op_cd, od_id, kn_created_at
FROM g5_shop_kcp_noti WHERE kn_done = 0 ORDER BY kn_created_at;

미완료 행을 단순 삭제하거나 kn_done=1로 바꾸면 중복 가산 또는 미반영 통보 유실이 발생할 수 있다. 주문·개인결제·장바구니의 현재 값과 kn_plan, KCP 거래 기록을 대조하고 충돌 원인을 해결한 뒤 동일 통보를 재전송한다. 이력에는 금액·계좌 등 거래 정보가 포함되므로 DB 백업과 접근권한을 기존 주문 데이터와 같이 관리한다.

검증

python3 tests/kcp_notification_http.py 33443
php bin/check-migration-schema.php

첫 명령은 격리 MySQL 전용이다. 로컬 지정 포트에 root/빈 비밀번호로 접속하여 고유 임시 DB를 만들고 종료 시 삭제한다. PHP CLI·mysqli, MySQL CLI, Python 3이 필요하다. bin의 스키마 검사는 로컬 검증 도구로 배포본에 포함되지 않는다.

테스트는 실제 콜백 파일을 로컬 HTTP로 호출하고 MySQL 8의 MyISAM/InnoDB 테이블을 읽고 쓴다. 공통 초기화와 PG 발신 IP는 테스트 부트스트랩으로 대체한다. 주문·개인결제는 격리 DB에 준비하며 실제 KCP 승인·은행 입금·상점 로그인·전체 주문서 제출을 실행하지 않는다. 각 SQL 쓰기 직전 실패와 DB 반영 후 응답 유실은 테스트 SQL 래퍼에서 주입한다. 정상/거부 응답과 실제 DB 상태, 동시 통보, 중복·망취소·역순, 중간 실패 복구와 외부 수정 충돌, 실제 마이그레이션 적용·재실행을 확인한다.

2026-09-10 검증 결과: PHP 8.4.22, MySQL 8.0.45에서 HTTP·DB 검사 420개와 PHP 문법 검사, 신규 설치/마이그레이션 스키마 대조가 통과했다.

Windows CRLF 설치본 업그레이드 보완

추가 설치 검증에서 기존 @skip-if-column 파서가 CRLF 줄 끝을 인식하지 못해, 이미 존재하는 cf_email_certify_minutes 컬럼을 다시 추가하려다 오류 1060으로 중단되는 문제가 재현됐다. 이 선행 오류 때문에 KCP 마이그레이션까지 실행되지 않았다. 파서가 CRLF 및 줄 끝 공백·탭을 허용하도록 수정했다. 인자 구분은 수평 공백으로 제한하여 불완전한 지시문이 다음 줄을 소비하지 않도록 했으며, SQL 파일과 체크섬 산정 방식은 변경하지 않았다.

php tests/migration_line_endings.php로 LF·CRLF, 줄 끝 공백·탭, 파일 끝 지시문, 불완전한 지시문 및 원본 체크섬 보존을 포함한 13개 검사가 통과했다.

PHP 8.4.25·MySQL 8.0.46의 별도 Windows 프로세스와 전용 DB에서 수정 전 소스를 실제 설치하고, 비회원 PC·회원 모바일 HTTP 주문 2건을 저장한 뒤 수정 소스를 적용했다. 원본 CRLF SQL을 유지한 상태에서 실제 관리자 로그인·CSRF 토큰을 사용한 전체 DB 업그레이드 및 KCP 마이그레이션 재실행이 통과했다. 기존 주문 금액·상태와 과거 상점코드 빈값이 유지됐고, 통보 테이블 컬럼이 신규 설치본과 일치했다. 결과는 tests/reports/kve-2026-2346-migration-fixed.json에 기록했다. VM·컨테이너 및 실제 KCP 발급·입금 연동 시험은 포함하지 않았다.

배포·회신 상태

수정 코드는 5.6.38 배포에 포함한다. 신고 대상은 5.6.35이며 검토 기준 5.6.37 개발 코드에도 원인이 있었다. 전체 영향 버전 범위는 추가 확인이 필요하다. 실제 KCP 연동 시험, 구형 발신 범위 확인, 운영 반영, KISA 회신 완료를 의미하지 않는다. 원본 PDF는 저장소에 포함하지 않는다.

규격 참고: 가상계좌 입금·망취소, 웹훅 응답. 공개 규격에서 확인되지 않은 수신 서명 필드를 임의로 필수화하지 않았다. 인증서·서명을 사용하는 서버 간 거래조회 API의 도입은 사용 중인 계약/연동 지원 확인 후 별도로 검토한다.