fix(security): KVE-2026-1914/1915/1919 remediation 전건

위임 관리자(부관리자)가 권한·역할·표현식·비밀 콘텐츠 경계를 우회하던
결함군을 계층 대칭성 원칙으로 전건 차단한다. 약한 경로가 정상 응답을
내보내는 것이 유일한 증상이라, 게이트를 생산 지점 한 곳(SSoT)에 두고
같은 데이터를 내보내는 소비 경로 전부가 그 게이트를 경유하도록 맞췄다.

- 등급 상한(rank ceiling): 슈퍼관리자 보호·역할/사용자 역할 배정(추가·제거
 대칭)·일괄 상태변경·순서변경을 상세 경로와 동일 강도로 재적용. 가드는
 DB 쓰기에 선행하여 거부 시 상태 불변.
- 레이아웃 표현식: new Function/with 실행을 AST 화이트리스트 평가기로 교체.
 비-문자열 computed 키 정규화(normalizeKey)·Object facade(리플렉션 static
 제거)·legacy 접근자 차단. 저장측 검증·정적 검사와 3계층 동형.
- secret 게이트: 비밀글의 댓글·첨부·문의 독립 경로 재적용, hash 파일서빙
 소유권·비밀·발행 상태 검사 통일.
- 신뢰 스크립트 호스트: 확장 선언 기반 + same-origin 브라우저 정규화를
 런타임·저장측·정적검사 3층 동형화.
- 회귀 감지: 단위·Feature·E2E·시나리오 매니페스트 전축 + audit 룰 4종 신설.
This commit is contained in:
HeuJung
2026-08-17 01:45:09 +09:00
parent cd84b63d2b
commit 71abdeaf60
249 changed files with 14354 additions and 1134 deletions
@@ -129,6 +129,7 @@ describe('Admin 본인인증 모달 cancel 흐름 (sirsoft-admin_basic)', () =>
expect(onError[0].handler).toBe('suppress');
});
/** @effects admin_basic_modal_cancel_flow_matches_basic_template */
it('cancel sequence 가 [apiCall, resolveIdentityChallenge, closeModal] 순서', () => {
const seq = findCancelSequence();
const inner = (seq!.params!.actions ?? []) as Action[];
@@ -543,6 +543,7 @@ describe('코드 편집기 ?route= URL 동기화', () => {
expect(routeInit.params.selectedNodeKind).toBe('layout');
});
/** @effects code_editor_file_select_syncs_route_query */
it('데스크탑 파일 항목 클릭 시 replaceUrl 로 file.route_path 를 ?route= 에 반영한다', () => {
// 좌측 트리 레이아웃 파일 iteration (file_var) 의 click 시퀀스
const fileIteration = findComponent(layoutEditJson, (n) =>
@@ -567,6 +568,7 @@ describe('코드 편집기 ?route= URL 동기화', () => {
expect(replaceUrl.params.query.route).toContain('route_path');
});
/** @effects code_editor_extension_select_clears_route_query */
it('확장 선택 시 replaceUrl 로 ?route= 를 비운다 (확장은 라우트 없음)', () => {
const extIteration = findComponent(layoutEditJson, (n) =>
Boolean(n.iteration?.source?.includes('extGroup.extensions'))
@@ -248,6 +248,7 @@ describe('본인인증 공통 모달 (sirsoft-basic) — engine-v1.46.0', () =>
expect(cancelChain).toBeDefined();
});
/** @effects modal_cancel_button_calls_server_cancel_api_first, modal_cancel_apiCall_uses_global_identityChallenge_challenge_id */
it('cancel sequence 의 첫 액션이 서버 cancel API 호출 — audit trail 정합', () => {
const seq = findCancelSequence();
expect(seq).toBeDefined();
@@ -262,6 +263,7 @@ describe('본인인증 공통 모달 (sirsoft-basic) — engine-v1.46.0', () =>
expect(apiCall!.target!).toContain('_global.identityChallenge.challenge_id');
});
/** @effects modal_cancel_apiCall_skipped_when_challenge_id_absent */
it('cancel apiCall 이 challenge 발급된 경우에만 호출 (if 가드)', () => {
const seq = findCancelSequence();
const inner = (seq!.params!.actions ?? []) as Action[];
@@ -269,6 +271,7 @@ describe('본인인증 공통 모달 (sirsoft-basic) — engine-v1.46.0', () =>
expect((apiCall as any).if).toContain('_global.identityChallenge?.challenge_id');
});
/** @effects modal_cancel_apiCall_failure_does_not_block_modal_close */
it('cancel apiCall 실패해도 모달 닫기는 진행 (onError suppress)', () => {
const seq = findCancelSequence();
const inner = (seq!.params!.actions ?? []) as Action[];
@@ -278,6 +281,7 @@ describe('본인인증 공통 모달 (sirsoft-basic) — engine-v1.46.0', () =>
expect(onError[0].handler).toBe('suppress');
});
/** @effects modal_cancel_sequence_order_is_apiCall_resolve_close */
it('cancel sequence 가 [apiCall, resolveIdentityChallenge, closeModal] 순서', () => {
const seq = findCancelSequence();
const inner = (seq!.params!.actions ?? []) as Action[];
@@ -41,6 +41,7 @@ describe('Footer — G7 표준 responsive 분기', () => {
vi.clearAllMocks();
});
/** @scenario preview_device=desktop, custom_width_input=in_range */
it('데스크톱(1280) — 링크 그룹 그리드 5열', () => {
setWidth(1280);
const { container } = render(<Footer />);
@@ -49,6 +50,7 @@ describe('Footer — G7 표준 responsive 분기', () => {
expect(grid.style.gridTemplateColumns).toBe('repeat(5, minmax(0, 1fr))');
});
/** @scenario preview_device=tablet, custom_width_input=in_range */
it('태블릿(820) — 그리드 2열', () => {
setWidth(820);
const { container } = render(<Footer />);
@@ -56,6 +58,7 @@ describe('Footer — G7 표준 responsive 분기', () => {
expect(grid.style.gridTemplateColumns).toBe('repeat(2, minmax(0, 1fr))');
});
/** @scenario preview_device=mobile, custom_width_input=in_range */
it('모바일(390) — 그리드 1열 + 저작권 영역 세로(column)', () => {
setWidth(390);
const { container } = render(<Footer />);