본문으로 건너뛰기

사은품 주문 보안 강화.

PHP 7 · GODO25
변경 파일 수: 4개
변경 파일 목록
4
변경 파일 코드
추가삭제
Component/Gift/Gift.php+90
변경 전Gift.php
}
return $isStrip ? gd_htmlspecialchars_stripslashes($setData) : $setData;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후Gift.php
}
return $isStrip ? gd_htmlspecialchars_stripslashes($setData) : $setData;
}
 
/**
* 주문 사은품 데이터 검증 및 정규화 (변조 방지)
*
* 주문서에서 전달된 사은품 hidden 필드(giveCnt/selectCnt/giftNo)는 개발자도구 등으로
* 변조가 가능하므로, 서버에서 재조회한 유효 사은품 정보($giftInfo)를 기준으로 검증·정규화
* - giveCnt(지급수량) : 지급조건의 값으로 강제 정규화 (변조 무력화, 주문은 정상 생성)
* - selectCnt(선택수량) : 실제 선택한 사은품 개수가 지급조건과 다르면 차단 (전체=0 은 제외)
* - 지급조건에 포함되지 않은 사은품(giftNo)은 제거
*
* @param array $submittedGift 주문서에서 전달된 사은품 데이터 [presentSno][index] => [...]
* @param array $giftInfo getGiftPresentOrder() 로 조회한 유효 사은품 정보
* @return array 정규화된 사은품 데이터
* @throws \Exception
*/
public function validateAndNormalizeOrderGift(array $submittedGift, array $giftInfo): array
{
if (empty($submittedGift) || empty($giftInfo)) {
return $submittedGift;
}
 
foreach ($submittedGift as $presentSno => $giftData) {
// 유효하지 않은 사은품 정책은 기존 저장 로직에서 스킵되므로 건드리지 않음
if (!array_key_exists($presentSno, $giftInfo) || empty($giftInfo[$presentSno]['gift'])) {
continue;
}
 
// 권위 데이터 집계 : giftNo => 지급조건(row), 지급조건별 giveCnt/selectCnt
$authGiveCnt = []; // giftNo => giveCnt
$giftNoToRow = []; // giftNo => rowKey
$rowSelectCnt = []; // rowKey => selectCnt
foreach ($giftInfo[$presentSno]['gift'] as $rowKey => $row) {
$rowSelectCnt[$rowKey] = (int) ($row['selectCnt'] ?? 0);
if (empty($row['multiGiftNo']) || !is_array($row['multiGiftNo'])) {
continue;
}
foreach ($row['multiGiftNo'] as $item) {
if (!isset($item['giftNo'])) {
continue;
}
$authGiveCnt[$item['giftNo']] = (int) ($row['giveCnt'] ?? 0);
$giftNoToRow[$item['giftNo']] = $rowKey;
}
}
 
// 제출된 사은품 정규화 + 지급조건별 선택 개수/제출 선택수량 집계
$selectedCntByRow = [];
$submittedSelectCntByRow = [];
foreach ($giftData as $index => $gift) {
// 선택되지 않은 항목(giftNo 없음)은 통과 (기존 로직이 저장하지 않음)
if (!isset($gift['giftNo']) || $gift['giftNo'] === '') {
continue;
}
// 지급조건에 포함되지 않은 사은품(변조)은 제거
if (!isset($authGiveCnt[$gift['giftNo']])) {
\Logger::channel('order')->info(__METHOD__ . ' 지급조건에 없는 사은품 제거', [$presentSno, $gift['giftNo']]);
unset($submittedGift[$presentSno][$index]);
continue;
}
// giveCnt(지급수량) 정규화 - POST 변조 무력화
$submittedGift[$presentSno][$index]['giveCnt'] = $authGiveCnt[$gift['giftNo']];
 
$rowKey = $giftNoToRow[$gift['giftNo']];
$selectedCntByRow[$rowKey] = ($selectedCntByRow[$rowKey] ?? 0) + 1;
// 제출된 선택수량(변조 탐지용) - 같은 지급조건 항목은 동일 값
if (isset($gift['selectCnt'])) {
$submittedSelectCntByRow[$rowKey] = (int) $gift['selectCnt'];
}
}
 
// 선택수량(selectCnt) 검증
foreach ($rowSelectCnt as $rowKey => $selectCnt) {
$selectedCnt = (int) ($selectedCntByRow[$rowKey] ?? 0);
$submittedSelectCnt = $submittedSelectCntByRow[$rowKey] ?? null;
$message = GiftSelectCntValidator::resolveViolationMessage($selectCnt, $selectedCnt, $submittedSelectCnt);
if ($message !== null) {
\Logger::channel('order')->warning(__METHOD__ . ' 사은품 선택수량 검증 실패', [
'presentSno' => $presentSno,
'rowKey' => $rowKey,
'authSelectCnt' => $selectCnt,
'selectedCnt' => $selectedCnt,
'submittedSelectCnt' => $submittedSelectCnt,
]);
throw new \Exception($message);
}
}
}
 
return $submittedGift;
}
}
 
Component/Gift/GiftSelectCntValidator.php+55
변경 전GiftSelectCntValidator.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후GiftSelectCntValidator.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Component\Gift;
 
/**
* 사은품 선택수량(selectCnt) 검증
*
* DB/세션/로거 등 부수효과 없이 정수 입력만으로 위반 여부를 판정
* 일반주문 등 각 호출부는 이 메시지를 받아 흐름에 맞는 예외를 던진다
*/
class GiftSelectCntValidator
{
/**
* 선택수량 위반 메시지를 반환한다. 위반이 없으면 null
*
* - 지급조건 선택수량(전체=0)은 검증 제외
* - 미선택(0개)은 검증 제외 (사은품 거절 허용)
* - 실제 선택 개수 또는 제출된 선택수량(변조)이 지급조건과 다르면 위반
*
* @param int $authSelectCnt 서버(지급조건)의 선택수량. 0이면 전체 지급
* @param int $selectedCnt 실제 선택(체크)된 사은품 개수
* @param int|null $submittedSelectCnt 주문서에서 제출된 선택수량 값(변조 탐지용). null이면 미사용
* @return string|null 위반 메시지 또는 null
*/
public static function resolveViolationMessage(int $authSelectCnt, int $selectedCnt, int $submittedSelectCnt = null)
{
// 전체(0) 또는 미선택은 검증 제외 (전체 지급 / 사은품 거절 허용)
if ($authSelectCnt <= 0 || $selectedCnt <= 0) {
return null;
}
 
// 제출된 선택수량이 없으면 실제 선택 개수로 비교
if ($submittedSelectCnt === null) {
$submittedSelectCnt = $selectedCnt;
}
 
// 최대 초과 : 실제 선택 개수 또는 제출된 선택수량(변조)이 지급조건보다 큰 경우
if ($selectedCnt > $authSelectCnt || $submittedSelectCnt > $authSelectCnt) {
return sprintf(__('사은품은 최대 %s개만 선택하실 수 있습니다.'), $authSelectCnt);
}
 
// 최소 미달 : 실제 선택 개수 또는 제출된 선택수량(변조)이 지급조건보다 작은 경우
if ($selectedCnt < $authSelectCnt || $submittedSelectCnt < $authSelectCnt) {
return sprintf(__('사은품은 최소 %s개 이상 선택하셔야 합니다.'), $authSelectCnt);
}
 
return null;
}
}
 
Component/Order/OrderNew.php+8
변경 전OrderNew.php
$giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], 0, false, false); // 수기 주문 케이스는 이 구간을 거치지 않음
\Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo);
 
 
 
 
 
 
 
 
 
if (count($giftInfo) > 0) {
foreach ($orderInfo['gift'] as $presentSno => $giftData) {
// 주문시 사용된 사은품 정보가 유효하지 않으면 삭제
 
변경 후OrderNew.php
$giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], 0, false, false); // 수기 주문 케이스는 이 구간을 거치지 않음
\Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo);
 
// 사은품 지급/선택 수량 검증 및 정규화 (giveCnt 변조 무력화 / selectCnt 위반 시 차단은 프론트 주문만, 관리자 수기주문 제외)
// 존재 여부 가드 - 미존재 시 fatal 대신 검증 미적용 + 경고 로그
if (method_exists($giftPresent, 'validateAndNormalizeOrderGift')) {
$orderInfo['gift'] = $giftPresent->validateAndNormalizeOrderGift($orderInfo['gift'], $giftInfo);
} else {
\Logger::channel('order')->warning(__METHOD__ . ' validateAndNormalizeOrderGift 미존재(Gift.php 튜닝본) - 사은품 수량 변조 검증 미적용', ['orderNo' => $this->orderNo]);
}
 
if (count($giftInfo) > 0) {
foreach ($orderInfo['gift'] as $presentSno => $giftData) {
// 주문시 사용된 사은품 정보가 유효하지 않으면 삭제
 
Component/Order/Order.php+12
변경 전Order.php
$giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], $groupSno, $isWrite, false);
\Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo);
 
 
 
 
 
 
 
 
 
 
 
 
 
if (count($giftInfo) > 0) {
foreach ($orderInfo['gift'] as $presentSno => $giftData) {
// 주문시 사용된 사은품 정보가 유효하지 않으면 삭제
 
변경 후Order.php
$giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], $groupSno, $isWrite, false);
\Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo);
 
// 사은품 지급/선택 수량 검증 및 정규화 (giveCnt 변조 무력화 / selectCnt 위반 시 차단)
// 프론트/회원 주문($isWrite === false)만 검증한다. 관리자 수기주문($isWrite === true)은
// selectCnt 강제(차단) 대상이 아니므로 제외한다.
if ($isWrite === false) {
// 존재 여부 가드 - 미존재 시 fatal 대신 검증 미적용 + 경고 로그
if (method_exists($giftPresent, 'validateAndNormalizeOrderGift')) {
$orderInfo['gift'] = $giftPresent->validateAndNormalizeOrderGift($orderInfo['gift'], $giftInfo);
} else {
\Logger::channel('order')->warning(__METHOD__ . ' validateAndNormalizeOrderGift 미존재(Gift.php 튜닝본) - 사은품 수량 변조 검증 미적용', ['orderNo' => $this->orderNo]);
}
}
 
if (count($giftInfo) > 0) {
foreach ($orderInfo['gift'] as $presentSno => $giftData) {
// 주문시 사용된 사은품 정보가 유효하지 않으면 삭제