Files
Gnuboard7/plugins/_bundled/sirsoft-tosspayments/resources/js/index.ts
T
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

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,
};