KISA 제보 3건(KVE-2026-2010/2011/2018)과 그 동일 계열 형제 결함을 전수 조치하고, 그 과정에서 드러난 두 결함군을 함께 닫는다. - 검증 시점과 연결 시점이 host 를 다르게 읽던 SSRF 통로를 정규화 SSoT 한 곳으로 모았다 - 세션을 여는 지점(2FA 완료·토큰 재발급)이 잠금 검사를 거치지 않아 계정 잠금이 우회됐다 - 인증도 서명도 없는 브라우저 리턴 콜백이 주문 상태를 바꾸던 통로를 4 PG 전부에서 닫고, 소유권을 대조하는 close-report 를 토스에도 신설했다. 그 결과 정리 주체를 잃는 결제창 미완료 주문은 만료 자동취소가 거둔다 - 저장소 A(_local)에만 쓰는 경로가 B 의 값을 조용히 덮던 회귀를 정본 writer 로 닫았다 (engine-v1.63.5). 한 방향만 보던 정적 검사에 반대 방향 축과 양방향 계약 테스트를 더했다 - 레이아웃 JSON 의 같은 객체 중복 키가 앞선 선언을 오류 없이 삼키던 결함군을 닫았다
120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
/**
|
|
* 토스페이먼츠 플러그인 엔트리
|
|
*
|
|
* ActionDispatcher에 핸들러를 등록하여
|
|
* 레이아웃 JSON에서 "sirsoft-tosspayments.requestPayment" 형태로 호출할 수 있게 합니다.
|
|
*/
|
|
|
|
import { handlerMap } from './handlers';
|
|
import { installAdminPaymentMethodBrandInjector } from './adminPaymentMethodBrandInjector';
|
|
import { reportPaymentFailureOnReturn } from './paymentCloseReport';
|
|
|
|
const PLUGIN_IDENTIFIER = 'sirsoft-tosspayments';
|
|
|
|
/**
|
|
* 로거 (G7Core 초기화 전에도 동작)
|
|
*/
|
|
const logger = {
|
|
info: (...args: unknown[]) => console.info(`[${PLUGIN_IDENTIFIER}]`, ...args),
|
|
warn: (...args: unknown[]) => console.warn(`[${PLUGIN_IDENTIFIER}]`, ...args),
|
|
error: (...args: unknown[]) => console.error(`[${PLUGIN_IDENTIFIER}]`, ...args),
|
|
};
|
|
|
|
/**
|
|
* ActionDispatcher에 핸들러를 등록합니다.
|
|
*
|
|
* @returns 등록된 핸들러 수
|
|
*/
|
|
function registerHandlers(): number {
|
|
const g7Core = (window as Record<string, unknown>).G7Core as Record<string, unknown> | undefined;
|
|
|
|
if (!g7Core) {
|
|
return 0;
|
|
}
|
|
|
|
const getDispatcher = g7Core.getActionDispatcher as (() => Record<string, unknown>) | undefined;
|
|
|
|
if (typeof getDispatcher !== 'function') {
|
|
return 0;
|
|
}
|
|
|
|
const dispatcher = getDispatcher() as Record<string, unknown> | undefined;
|
|
|
|
if (!dispatcher || typeof dispatcher.registerHandler !== 'function') {
|
|
return 0;
|
|
}
|
|
|
|
let count = 0;
|
|
for (const [name, handler] of Object.entries(handlerMap)) {
|
|
const fullName = `${PLUGIN_IDENTIFIER}.${name}`;
|
|
dispatcher.registerHandler(fullName, handler, {
|
|
category: 'plugin',
|
|
source: PLUGIN_IDENTIFIER,
|
|
});
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* 플러그인 초기화
|
|
*
|
|
* DOMContentLoaded 후 ActionDispatcher가 준비되면 핸들러를 등록합니다.
|
|
* ActionDispatcher가 아직 준비되지 않았으면 최대 5초간 재시도합니다.
|
|
*/
|
|
function initPlugin(): void {
|
|
// 관리자 주문설정의 브랜드 마크 주입 — 핸들러 등록 성공 여부와 무관하게 설치한다
|
|
// (관리자 화면은 결제 핸들러를 쓰지 않으므로 ActionDispatcher 준비를 기다릴 이유가 없다).
|
|
installAdminPaymentMethodBrandInjector();
|
|
|
|
// 결제 실패로 돌아온 화면이면 서버에 보고한다. 브라우저 리턴 콜백은 인증이 없어 주문
|
|
// 상태를 바꾸지 않으므로, 소유권을 대조하는 close-report 가 정당한 실패를 기록하는
|
|
// 유일한 경로다. 저장해 둔 정보가 없으면 아무 일도 하지 않는다.
|
|
void reportPaymentFailureOnReturn();
|
|
|
|
const doInit = () => {
|
|
const count = registerHandlers();
|
|
|
|
if (count > 0) {
|
|
logger.info(`${count} handler(s) registered`);
|
|
return;
|
|
}
|
|
|
|
// ActionDispatcher 미준비 시 재시도 (100ms 간격, 최대 50회 = 5초)
|
|
let retries = 0;
|
|
const maxRetries = 50;
|
|
const interval = setInterval(() => {
|
|
retries++;
|
|
const result = registerHandlers();
|
|
|
|
if (result > 0) {
|
|
clearInterval(interval);
|
|
logger.info(`${result} handler(s) registered (after ${retries} retries)`);
|
|
return;
|
|
}
|
|
|
|
if (retries >= maxRetries) {
|
|
clearInterval(interval);
|
|
logger.warn('ActionDispatcher not available after timeout');
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', doInit);
|
|
} else {
|
|
doInit();
|
|
}
|
|
}
|
|
|
|
// 자동 초기화
|
|
initPlugin();
|
|
|
|
// 디버그용 글로벌 노출
|
|
(window as Record<string, unknown>).__SirsoftTosspayments = {
|
|
identifier: PLUGIN_IDENTIFIER,
|
|
handlers: Object.keys(handlerMap),
|
|
initPlugin,
|
|
};
|