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

83 lines
4.8 KiB
TypeScript

/**
* Layout Editor — 팔레트 defaultNode 공통 디자인 className 반영 회귀 가드.
*
* 배경:
* - 팔레트에서 컴포넌트를 추가하면 editor-spec.json 의 defaultNode 가 캔버스 트리에 삽입된다.
* - 이전에는 defaultNode 가 placeholder(inline style / dashed border) 수준이라 추가된 요소가
* 템플릿의 실제 Tailwind 디자인과 동떨어졌다. 본 작업에서 defaultNode.props.className 을
* 운영 레이아웃의 대표 패턴으로 채웠다.
* - 본 spec 은 편집기에서 Button / Input 을 추가했을 때 캔버스에 렌더된 요소가 placeholder 가
* 아닌 공통 디자인 className 으로 렌더되는지 검증한다. (Vitest 는 JSON+격리 렌더, 본 spec 은
* 실제 편집기 chrome + 캔버스 통합 렌더 — feedback_wysiwyg_preview_user_page_parity)
*
* 두 번들 템플릿 모두 검증 (T8 양쪽 변종):
* - sirsoft-basic: Button → rounded-lg, Input → border 패턴
* - sirsoft-admin_basic: Button → bg-blue-600, Input → "input" 유틸 클래스
*
* 축 요약(마커 아님 — 평문): palette_open, default_node_source:editorSpecDefaultNode.
* 효과 요약(마커 아님 — 평문): palette_default_node_className_replicated_into_inserted_node.
* 효과 요약(마커 아님 — 평문): palette_default_node_className_reflects_template_design_system_tokens.
* 효과 요약(마커 아님 — 평문): palette_default_node_button_omits_variant_size_uses_className_only.
*/
import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth';
type Probe = { template: string; component: string; token: string };
const probes: Probe[] = [
{ template: 'sirsoft-basic', component: 'Button', token: 'rounded-lg' },
{ template: 'sirsoft-basic', component: 'Input', token: 'border' },
{ template: 'sirsoft-admin_basic', component: 'Button', token: 'bg-blue-600' },
{ template: 'sirsoft-admin_basic', component: 'Input', token: 'input' },
];
test.describe('@layout-editor defaultNode 공통 디자인 반영', () => {
for (const { template, component, token } of probes) {
/** @effects palette_default_node_className_replicated_into_inserted_node, palette_default_node_className_reflects_template_design_system_tokens, palette_default_node_button_omits_variant_size_uses_className_only */
test(`${template} — ${component} 추가 시 캔버스에 "${token}" className 반영`, async ({ page }) => {
const tokenAuth = issueToken('core.templates.layouts.edit');
await authenticatePage(page, tokenAuth);
await page.goto(`/admin/layout-editor/${template}`);
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
// 로케일 비종속 — testid 로 "요소 추가" 버튼을 잡는다 (편집기 chrome 은 admin 로케일에 따라 ko/en 모두 가능)
const addElementBtn = page.getByTestId('g7le-toolbar-add-element');
await addElementBtn.waitFor({ state: 'visible', timeout: 30_000 });
// 라우트 트리에서 실제 편집 가능한 leaf 라우트를 선택해야 팔레트/캔버스가 렌더된다.
// redirect 라우트(badge-redirect 동반)는 레이아웃이 없어 "Select a route..." 안내만 뜨므로 제외.
// route 트리 path 형식이 템플릿마다 달라 ?route= 파라미터 대신 트리 항목을 직접 클릭.
const editableRouteItem = page
.getByTestId('g7le-route-tree-item')
.filter({ hasNot: page.getByTestId('g7le-route-tree-badge-redirect') })
.first();
await editableRouteItem.waitFor({ state: 'visible', timeout: 30_000 });
await editableRouteItem.click();
await expect(addElementBtn).toBeEnabled({ timeout: 30_000 });
// 캔버스 내 해당 className 요소의 삽입 전 개수 (거짓 양성 회피 — 증가분을 검증)
// token 은 단순 클래스명이라 CSS.escape 불필요 (테스트 본문은 Node 컨텍스트 — CSS 전역 없음)
const countSelector = `[data-testid="g7le-preview-canvas"] .${token}`;
const before = await page.locator(countSelector).count();
// 팔레트 열고 대상 컴포넌트 추가
await addElementBtn.click();
const paletteItem = page.getByTestId(`g7le-palette-item-${component}`);
await paletteItem.waitFor({ state: 'visible', timeout: 10_000 });
await paletteItem.click();
// 삽입 후 캔버스에 해당 className 요소가 증가해야 함 (defaultNode className 복제)
await expect
.poll(
async () => page.locator(countSelector).count(),
{
message: `${component} 추가 후 "${token}" className 요소가 캔버스에서 증가해야 함 (defaultNode className 복제)`,
timeout: 15_000,
},
)
.toBeGreaterThan(before);
});
}
});