Files
HeuJung 71abdeaf60 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종 신설.
2026-08-17 01:45:09 +09:00

73 lines
3.4 KiB
TypeScript

/**
* Layout Editor — 번들 분리 (lazy 로드) 검증. (engine-v1.51.0)
*
* 코어 번들(template-engine.min.js)에서 편집기 코드를 분리해 별도 layout-editor.min.js 로
* 빌드하고, `/admin/layout-editor/*` 진입 시에만 런타임 <script> 주입으로 로드한다.
*
* 검증:
* - 일반 admin 페이지(대시보드) 접속 → layout-editor.min.js 요청이 발생하지 않는다
* (초기 접속 payload 축소의 핵심 — 회귀 시 편집기 코드가 다시 모든 페이지에 실린다)
* - 편집기 URL 진입 → layout-editor.min.js 요청 발생 + 편집기 마운트(g7le-toolbar +
* g7le-preview-frame). React 사본이 둘이면 "Invalid hook call" 로 마운트 자체가 크래시하므로,
* 마운트 성공이 곧 단일 React 인스턴스 + 컨텍스트/싱글톤 동일성(shim __runtime 공유)을 증명한다.
*
* @scenario bundle_split_lazy_editor
* 효과 요약(마커 아님 — 평문): editor_bundle_absent_on_normal_page, editor_bundle_loaded_and_mounted_on_editor_route.
*/
import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth';
type PwPage = import('@playwright/test').Page;
const EDITOR_BUNDLE_RE = /layout-editor\.min\.js/;
/**
* page 의 네트워크 요청 중 편집기 번들 요청 URL 을 수집하는 리스너를 설치한다.
*
* @param page Playwright page
* @returns 수집된 편집기 번들 요청 URL 배열(참조 — 이후 계속 채워짐)
*/
function collectEditorBundleRequests(page: PwPage): string[] {
const hits: string[] = [];
page.on('request', (req) => {
if (EDITOR_BUNDLE_RE.test(req.url())) {
hits.push(req.url());
}
});
return hits;
}
test.describe('Layout Editor — 번들 분리 (lazy 로드)', () => {
/** @effects editor_bundle_absent_on_normal_page */
test('일반 admin 페이지는 layout-editor.min.js 를 요청하지 않는다', async ({ page }) => {
const token = issueToken('core.templates.layouts.edit');
await authenticatePage(page, token);
const editorRequests = collectEditorBundleRequests(page);
await page.goto('/admin/dashboard');
await page.waitForLoadState('networkidle', { timeout: 30_000 });
// 초기 접속 payload 에 편집기 번들 미포함 — 회귀 방지 핵심
expect(editorRequests, '일반 페이지에서 편집기 번들이 로드됨(회귀)').toEqual([]);
});
/** @effects editor_bundle_loaded_and_mounted_on_editor_route */
test('편집기 URL 진입 시 layout-editor.min.js 를 로드하고 마운트한다', async ({ page }) => {
const token = issueToken('core.templates.layouts.edit');
await authenticatePage(page, token);
const editorRequests = collectEditorBundleRequests(page);
await page.goto('/admin/layout-editor/sirsoft-basic?route=%2F');
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
// 편집기 셸 마운트 대기 — 마운트 성공 = 단일 React 인스턴스 + 컨텍스트 동일성 증명
await page.waitForSelector('[data-testid="g7le-toolbar"]', { timeout: 30_000 });
await page.waitForSelector('[data-testid="g7le-preview-frame"]', { timeout: 30_000 });
// 편집기 진입 시에는 lazy 번들이 실제로 로드됐어야 한다
expect(editorRequests.length, '편집기 번들 요청 미발생').toBeGreaterThan(0);
expect(editorRequests[0]).toMatch(EDITOR_BUNDLE_RE);
});
});