Files
HeuJung 21fc114f37 fix(core,extensions): 보안 결함 3건과 이중저장소·레이아웃 중복키 결함군 폐쇄
KISA 제보 3건(KVE-2026-2010/2011/2018)과 그 동일 계열 형제 결함을 전수 조치하고,
그 과정에서 드러난 두 결함군을 함께 닫는다.

- 검증 시점과 연결 시점이 host 를 다르게 읽던 SSRF 통로를 정규화 SSoT 한 곳으로 모았다
- 세션을 여는 지점(2FA 완료·토큰 재발급)이 잠금 검사를 거치지 않아 계정 잠금이 우회됐다
- 인증도 서명도 없는 브라우저 리턴 콜백이 주문 상태를 바꾸던 통로를 4 PG 전부에서 닫고,
 소유권을 대조하는 close-report 를 토스에도 신설했다. 그 결과 정리 주체를 잃는
 결제창 미완료 주문은 만료 자동취소가 거둔다
- 저장소 A(_local)에만 쓰는 경로가 B 의 값을 조용히 덮던 회귀를 정본 writer 로 닫았다
 (engine-v1.63.5). 한 방향만 보던 정적 검사에 반대 방향 축과 양방향 계약 테스트를 더했다
- 레이아웃 JSON 의 같은 객체 중복 키가 앞선 선언을 오류 없이 삼키던 결함군을 닫았다
2026-09-02 17:36:11 +09:00

12 KiB

이커머스 — 설정·권한·라우트

설정 스키마·권한·메뉴·라우트·의존 관계 · 진입점: AGENTS.md

설정 스키마

getSettingsSchema() 선언이 없습니다.

기본값 파일: config/settings/defaults.json

getSettingsSchema() 선언이 없는 것은 누락이 아닙니다. 이 모듈의 설정은 코드가 아니라 config/settings/defaults.json 이 SSoT 이며, 그 파일 하나가 세 가지를 함께 담습니다:

키 역할
_meta.categories 설정 그룹 9개의 목록과 순서 (basic_info · language_currency · order_settings · shipping · seo · review_settings · inquiry · notifications · mileage)
defaults 그룹별 기본값. 설치 시 storage/app/settings/ 로 동기화되어 module_setting() 이 읽는 값이 됩니다
frontend_schema 관리자 화면이 자동으로 그리는 입력 폼 정의

frontend_schema 는 8개 그룹뿐이고 mileage 가 없습니다. 자동 생성 폼으로는 표현할 수 없는 입력(통화별 적립 규칙 표 등)이 있어서 마일리지 탭만 레이아웃 JSON 으로 직접 그리기 때문입니다 (resources/layouts/admin/partials/admin_ecommerce_settings/_tab_mileage*.json 6개). 새 그룹을 추가할 때는 이 셋 중 어디까지 손댈지를 먼저 정합니다 — _meta.categories 에만 넣고 defaults 를 빠뜨리면 그 그룹은 화면에 뜨지만 저장할 값이 없습니다.

설정 값을 코드에서 읽을 때는 EcommerceSettingsService 를 거칩니다. 통화 설정처럼 요청마다 여러 번 읽히는 값은 CurrencySettingsCache 가 따로 캐시하며, 설정 저장 후 캐시를 비우는 것은 core.module_settings.after_save 훅을 받는 리스너들입니다 — 설정을 직접 파일에서 읽으면 그 무효화 경로를 타지 않아 화면과 서버가 서로 다른 값을 봅니다.

결제수단 목록(order_settings.payment_methods)만은 성격이 다릅니다. 저장값과 플러그인이 등록한 카탈로그의 병합이라, 플러그인을 삭제·비활성화하면 저장값은 남아 있는데 카탈로그에서 사라지는 고아 항목이 생깁니다. 공개 응답은 고아 항목을 걸러 내보내고 관리자 응답은 그대로 노출하는 것이 규칙입니다 — 운영자는 그것을 보고 지워야 하기 때문입니다.

order_settings.pending_order_expire_minutes 는 결제창까지 갔으나 결제가 성립하지 않은 주문(PENDING_ORDER)을 만료 자동취소 대상에 넣는 기준입니다(기본 1440분 = 24시간). 0 이면 그 부류를 정리하지 않습니다.

이 값은 환경설정 > 주문 설정 > 주문 자동취소 카드에 있습니다. 형제 값 auto_cancel_days 와 달리 auto_cancel_expired 토글 하위가 아닙니다 — 정리 스케줄러가 그 토글과 무관하게 이 값을 읽기 때문에, 토글을 끈 상태에서 이 입력만 사라지면 화면이 실제 동작을 설명하지 못합니다. 입력 경계(0 ~ 20160분)는 config/ecommerce.php 의 limits 가 단일 출처이며, 화면은 설정 응답의 _meta.limits 로 같은 값을 받습니다.

이 부류를 정리 대상에 넣은 이유는 정리 주체가 아예 없었기 때문입니다. 기존 자동취소는 입금 기한(vbank_due_at·deposit_due_at)이 있는 결제수단만 훑고, 임시주문 정리는 다른 테이블을 봅니다. 브라우저 리턴 콜백이 주문 상태를 바꾸지 않게 된 뒤로는 승인 거절분도 여기에 머무르므로, 이 정리가 선차감 마일리지 복원의 최종 안전망입니다.

권한

카테고리 이름 액션 라우트 키
products 상품 관리 read, create, update, delete product
orders 주문 관리 read, update order
categories 카테고리 관리 read, create, update, delete -
brands 브랜드 관리 read, create, update, delete brand
product-notice-templates 상품정보제공고시 관리 read, create, update, delete -
product-common-infos 공통정보 관리 read, create, update, delete -
settings 환경설정 read, update -
promotion-coupon 쿠폰 관리 read, create, update, delete coupon
shipping-policies 배송정책 관리 read, create, update, delete shippingPolicy
product-labels 상품 라벨 관리 read, create, update, delete -
identity.policies 이커머스 본인인증 정책 read, update -
reviews 리뷰 관리 read, update, delete review
inquiries 문의 관리 update, delete -
dashboard 대시보드 view -
user-products 사용자 상품 read -
user-orders 사용자 주문 create, cancel, confirm -
user-reviews 사용자 리뷰 write -
mileage 마일리지 관리 read, manage mileage-transaction
user-currency 회원 결제 통화 관리 manage -
user-shipping-country 회원 배송국가 관리 manage -

권한 20종은 세 무리로 갈립니다.

  • 관리자 CRUD 12종 (products · orders · categories · brands · product-notice-templates · product-common-infos · promotion-coupon · shipping-policies · product-labels · reviews · inquiries · mileage): 관리자 화면과 1:1 대응하며, 라우트 키가 있는 것은 그 라우트에 스코프 미들웨어가 걸립니다.
  • 사용자 측 5종 (user-products · user-orders · user-reviews · user-currency · user-shipping-country): 구매자가 자기 자원에 대해 갖는 권한입니다. 관리자 권한과 이름이 겹치지 않도록 user- 접두사를 씁니다.
  • 횡단 3종 (settings · dashboard · identity.policies): 화면 하나에 대응합니다.

orders 에 create/delete 가 없는 것은 의도입니다 — 주문은 구매자 결제로 생기고 (user-orders.create), 삭제 대신 취소·환불로 처리합니다. inquiries 에 read 가 없는 것도 같은 성격입니다: 문의 본문은 게시판 모듈이 소유하므로 읽기 권한은 그 게시판의 권한이 정하고, 이 모듈은 답변·삭제만 관장합니다.

새 관리자 화면을 추가하면 권한·메뉴·라우트 이름 셋이 서로를 정확히 가리키는지 확인합니다. 권한만 추가하고 메뉴를 빠뜨리면 화면에 도달할 길이 없고, 반대면 눌러도 403 입니다.

메뉴

구분 slug 이름 URL 하위
관리자 sirsoft-ecommerce 이커머스 - 11개

최상위 sirsoft-ecommerce 아래 11개 하위 메뉴가 붙습니다 — 환경설정 · 상품 · 카테고리 · 브랜드 · 상품정보제공고시 · 공통정보 · 주문 · 쿠폰 · 배송정책 · 리뷰 · 마일리지 내역.

메뉴는 권한과 짝을 이룰 때만 보입니다. 운영자에게 역할이 부여되어도 그 역할에 해당 권한이 없으면 메뉴가 렌더되지 않으므로, 새 화면을 추가할 때는 getPermissions() 와 getAdminMenus() 를 함께 바꿉니다.

권한 표에는 있는데 메뉴가 없는 것들(dashboard · identity.policies · product-labels 등)은 독립 메뉴가 아니라 다른 화면 안에 들어 있기 때문입니다 — 대시보드는 코어 관리자 첫 화면에 레이아웃 조각으로 주입되고, 본인인증 정책은 코어 IDV 설정 화면에서 함께 다뤄지며, 상품 라벨은 상품 관리 화면 안에 있습니다.

라우트

종류 파일 URL prefix
api src/routes/api.php /api/modules/sirsoft-ecommerce/...

확장 라우트는 활성 상태인 확장의 것만 등록됩니다. 라우트 정의를 바꾸면 라우트 캐시 재생성이 필요합니다.

라우트 239개가 파일 하나(src/routes/api.php)에 모여 있고 전부 /api/modules/sirsoft-ecommerce/ 아래로 나갑니다. 화면용 라우트는 없습니다 — 관리자 화면은 레이아웃 JSON 이 이 API 를 호출해 그리고, 방문자 화면은 템플릿이 같은 API 를 씁니다.

대상별로 셋으로 갈립니다:

무리 인증 비고
admin.* 관리자 인증 + 권한 스코프 라우트 키가 선언된 권한이 여기에 걸립니다
user.* · 공개 조회 Sanctum(일부는 optional.sanctum) 비로그인도 상품·카테고리는 봅니다
guest.orders.* VerifyGuestOrderToken 비회원 주문 조회·취소. 토큰이 곧 신원이므로 새 라우트를 추가하면 미들웨어 선언(getMiddleware())에도 그 이름을 반드시 추가합니다

라우트를 추가·변경한 뒤에는 라우트 캐시를 다시 구워야 합니다. 확장 라우트는 활성 상태인 확장의 것만 등록되고, 캐시에 없는 라우트는 예외도 경고도 없이 그대로 404 가 됩니다.

모든 라우트에 name() 이 필요합니다 — 이름이 없으면 미들웨어 self-gate 의 targets 패턴과 IDV 정책의 라우트명 인덱스가 그 라우트를 찾지 못해, 보호가 걸린 것처럼 보이지만 실제로는 통과합니다.

의존 관계

이 확장이 의존하는 확장

없음 — 코어만으로 동작합니다.

이 확장에 의존하는 확장 (이 확장을 비활성화하면 함께 영향을 받습니다)

확장 유형 요구 버전
sirsoft-pay_kginicis 플러그인 >=1.1.0
sirsoft-pay_nhnkcp 플러그인 >=1.1.0
sirsoft-pay_nicepayments 플러그인 >=1.1.0
sirsoft-tosspayments 플러그인 >=1.1.0
sirsoft-basic 템플릿 >=1.1.0

이 모듈은 아무 확장에도 의존하지 않습니다. 코어만 있으면 동작하며, 관계는 전부 한 방향으로 들어옵니다 — 결제 플러그인 4종과 템플릿 sirsoft-basic 이 이 모듈을 요구합니다.

그 방향이 뒤집히지 않게 유지하는 것이 이 모듈 설계의 핵심입니다. PG 이름을 이 모듈 코드에 넣는 순간 의존이 양방향이 되고, 새 PG 를 붙일 때마다 이 모듈을 고쳐야 합니다.

manifest 에는 없지만 실제로 맞물리는 확장이 둘 더 있습니다:

확장 무엇으로 연결되는가 없으면
sirsoft-board 훅 3종 구독 (문의 글 삭제·복원·일괄삭제 시 피벗 정리) + 설정 inquiry.board_slug 상품 문의 기능만 비고 나머지는 정상
sirsoft-ckeditor5 훅 1종 (편집기가 참조할 이커머스 리소스 목록 제공) 편집기에서 상품 링크를 고를 수 없을 뿐

훅 구독은 상대가 없으면 발화하지 않으므로 이 둘을 manifest 의존으로 올리지 않는 것이 맞습니다. 다만 그 대가로 상대 확장이 훅 이름을 바꾸면 예외 없이 조용히 연동이 끊깁니다 — 상대의 docs/extension-points.md 를 함께 확인해야 하는 이유입니다.

이 모듈의 공개 표면(Service·Repository·Contracts·라우트·발행 훅)을 바꿀 때는 위 4+1 개 확장의 dependencies 최소 버전 상향이 필요한지 검토합니다.