Files
HeuJung 494e581973 fix(security): KVE-2026 신고 4건 조치 — 관리자 API 권한 게이트 + 금전 처리 경쟁 조건
KISA 신고 4건(KVE-2026-1886/1887/1893/1894)을 검증하고, 미수정 2건과
동종 결함 전수 조사에서 나온 결함을 함께 조치했다.

권한 게이트 (KVE-2026-1893)

- NHN KCP 관리자 주문 연동 7경로가 admin 보유 여부만 보고 세부 권한을 보지 않아,
 업무 권한 없는 관리자가 주문번호·결제정보·수령인 연락처를 조회하고 에스크로
 배송등록까지 할 수 있었다. 조회는 orders.read, 등록은 orders.update, 설정성
 경로는 settings.read 로 게이트해 다른 PG 연동과 강도를 맞췄다.
- 마케팅 채널 저장은 코어 플러그인 설정과 같은 저장소를 덮어쓰는 우회 경로라
 core.plugins.update 를 부착했다.

첨부 해시 노출 (KVE-2026-1894 잔재)

- 비밀글의 썸네일 URL 이 목록·상세 응답에 그대로 실려 첨부 해시가 노출됐다.
 이미지 서빙은 이미 차단돼 있었으나 식별값 자체가 나갔다. 첨부 목록과 같은
 게이트를 써서 값만 가리고 필드는 유지한다.

경쟁 조건 (KVE-2026-1886 + 전수 조사)

- 쿠폰 차감을 조회 후 갱신에서 조건부 UPDATE 로 바꿔 1회 제한 쿠폰의 동시
 사용을 막고, 선점당한 주문은 409 로 되돌린다.
- 라이브 병렬 재현에서 그 롤백이 동작하지 않는 것을 확인했다. Action 훅 기본값이
 큐 래핑 + afterCommit 이라 금전 처리가 호출자 커밋 뒤에 실행되고 있었다.
 쿠폰·적립금 차감/복원 5개 구독에 sync 를 선언했고, 회귀 테스트는 손 등록이
 아니라 실제 등록 경로를 태워 고정했다.
- 동시 부분취소의 취소 누적 컬럼 lost update, 적립 lot 의 read-modify-write,
 주문옵션당 적립 lot 중복 생성을 각각 행 잠금·컬럼 연산·유니크 제약으로 막았다.
 기설치본의 중복 lot 은 인덱스 생성 전에 금액을 합산해 한 줄로 통합한다.

규정·문서

- sync 판정 기준을 hooks.md 에 명문화하고 /AGENTS.md Listener 표와
 coverage manifest 에 반영했다. 같은 증상을 다시 만났을 때의 진단 경로를
 트러블슈팅 사례로 남겼다.
2026-08-22 01:19:36 +09:00

6.0 KiB

Channels API 레퍼런스

소유: plugin sirsoft-marketing · 생성: php artisan api:docgen (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다.


TL;DR (5초 요약)

1. 이 문서는 실제 API 호출로 실측한 Channels 엔드포인트 레퍼런스입니다
2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(raw HTTP) + 실측 응답 필드 표 + 응답 예시(envelope)
3. 응답 필드의 예시값·응답 예시 JSON 은 실제 호출 응답에서 관측된 값입니다
4. 갱신: 코드 변경 후 php artisan api:docgen 재실행
5. 설명(TODO) 칸은 사람이 채웁니다

PUT /api/plugins/sirsoft-marketing/admin/channels

  • 라우트명: api.plugins.sirsoft-marketing.admin.channels.update
  • 컨트롤러: Plugins\Sirsoft\Marketing\Http\Controllers\MarketingAdminController@updateChannels
  • 인증/권한: auth:sanctum

요청 파라미터

요청 파라미터 없음.

요청 예시

PUT /api/plugins/sirsoft-marketing/admin/channels HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}

응답 필드 (data 내부)

단건 응답: data 객체의 필드. data.channels 는 저장 후 확정된 채널 배열(시스템 채널 병합 결과)이며, 요청 body 의 channels 와 동일한 구조를 그대로 되돌려준다.

필드 타입 실측 예시값 용도/설명
channels array [{...}] 저장된 최종 채널 목록 (누락된 시스템 채널이 앞에 병합됨)
channels[].key string email_subscription 채널 식별자 (소문자·숫자·밑줄)
channels[].label object {"ko":"광고성 이메일 수신","en":"Email Marketing"} 로케일별 채널 라벨
channels[].label_key string sirsoft-marketing::channels.email_subscription.label 시스템 채널이 병합될 때만 포함되는 언어팩 라벨 키
channels[].page_slug string | null "" 연결 약관 페이지 slug (없으면 빈 문자열/null)
channels[].enabled boolean true 채널 노출 여부
channels[].is_system boolean true 시스템 채널 여부 (삭제·플래그 변경 불가)

응답 예시

{
    "success": true,
    "message": "채널이 저장되었습니다.",
    "data": {
        "channels": [
            {
                "key": "email_subscription",
                "label_key": "sirsoft-marketing::channels.email_subscription.label",
                "label": {
                    "ko": "광고성 이메일 수신",
                    "en": "Email Marketing"
                },
                "page_slug": "",
                "enabled": true,
                "is_system": true
            }
        ]
    }
}

에러 응답

상태코드 의미 발생 조건
401 Unauthenticated 유효한 Bearer 토큰이 없거나 만료된 경우
403 Forbidden 관리자가 아니거나 core.plugins.update 권한이 없는 경우
422 동의 이력 존재 삭제 대상 채널에 동의한 회원이 있는 경우 — "채널(:key)에 동의한 회원이 :count명 있어 삭제할 수 없습니다."

설명

관리자 환경설정 화면에서 마케팅 동의 채널 목록 전체를 한 번에 저장하는 엔드포인트다. 컨트롤러가 AdminBaseController 를 상속하므로 실제 인증은 auth:sanctum 에 더해 관리자(admin) 권한을 요구한다(생성기 표기는 auth:sanctum 만 노출). 제출된 배열이 곧 새 상태가 되며, 개별 채널 추가/수정 엔드포인트는 없다(전량 교체 방식).

이 엔드포인트는 코어의 PUT /api/admin/plugins/{identifier}/settings 와 같은 plugin_settings 를 덮어쓰므로, 라우트에 permission:admin,core.plugins.update 가 부착되어 있다. 관리자 계정이더라도 플러그인 설정 권한이 없으면 403 이 된다(생성기 표기에는 나타나지 않는다).

요청 파라미터는 생성기가 배열 중첩 규칙(channels.*)을 평면화하지 못해 위 표에 "없음"으로 표기되나, 실제 ChannelUpdateRequest 는 다음 body 를 요구한다:

이름 위치 타입 필수 허용값 용도
channels body array 예 최대 10개 저장할 채널 정의 배열(전체 교체)
channels.*.key body string 예 ^[a-z0-9_]+$ 채널 식별자(소문자·숫자·밑줄)
channels.*.label body object 예 로케일 맵, 각 값 최대 50자 다국어 라벨(LocaleRequiredTranslatable)
channels.*.page_slug body string 아니오 최대 100자 연결 약관 페이지 slug
channels.*.enabled body boolean 예 — 노출 여부
channels.*.is_system body boolean 예 — 시스템 채널 플래그
  • 시스템 채널 보호: is_system=true 채널은 제출 목록에서 빠져도 서버가 기본 정의를 다시 앞에 끼워 넣어 항상 유지된다. is_system 을 true→false 로 위변조하거나 시스템 채널을 누락시키면 검증 오류(422).
  • 동의 이력 보호: 이미 사용자 동의가 존재하는 채널을 목록에서 제거하려 하면 MarketingConsentService::countConsentedByKey() 가 0 이 아니어서 422 로 거부된다(개인정보 이력 보존).
  • key 중복 금지: 같은 key 가 둘 이상이면 422.
  • 성공 시 저장된 최종 channels 배열(시스템 채널 병합 포함)을 data.channels 로 되돌려준다.

응답 예시 (성공)

{
  "success": true,
  "data": {
    "channels": [
      { "key": "email_subscription", "label": { "ko": "광고성 이메일 수신", "en": "Email Marketing" }, "page_slug": "", "enabled": true, "is_system": true }
    ]
  },
  "message": "채널이 저장되었습니다.",
  "error": null
}