# 그누보드7 템플릿 엔진 보안 가이드 > 그누보드7 레이아웃 JSON 보안 아키텍처 — Custom Validation Rules, FormRequest 검증 계층, 공격 방어 전략 --- ## TL;DR (5초 요약) ```text 1. 다층 방어: FormRequest(10개 Custom Rule) → Service → Repository(ORM) → React(자동 이스케이프) 2. 10개 Custom Rule: JSON 구조, 컴포넌트, 엔드포인트, URL, 상속, 슬롯, 데이터소스, 권한, 경로, 파일타입 3. 7개 FormRequest: 용도별 Rule 조합 차등 적용 (Store/Update/Content/Inheritance/Preview/Get) 4. 상속 보안: 순환 참조 감지 + 깊이 10 제한 + 부모 슬롯/데이터소스 ID 고유성 검증 5. 확장 가능: 6개 Hook으로 모듈/플러그인이 검증 규칙을 동적으로 추가 가능 ``` --- ## 목차 1. [개요](#개요) 2. [보안 아키텍처](#보안-아키텍처) 3. [검증 계층](#검증-계층) 4. [Custom Validation Rules](#custom-validation-rules) 5. [FormRequest별 Rule 적용 현황](#formrequest별-rule-적용-현황) 6. [레이아웃 상속 보안](#레이아웃-상속-보안) 7. [경로 보안 (Path Traversal 방어)](#경로-보안-path-traversal-방어) 8. [Hook 기반 검증 확장](#hook-기반-검증-확장) 9. [공격 방어 전략](#공격-방어-전략) 10. [보안 모범 사례](#보안-모범-사례) 11. [관련 문서](#관련-문서) --- ## 개요 그누보드7 템플릿 엔진은 JSON 기반 레이아웃으로 화면을 동적으로 구성합니다. 이 유연성은 악의적 입력을 통한 공격 가능성을 내포하므로, **10개 Custom Validation Rule**과 **7개 FormRequest**를 조합한 다층 방어 체계로 보호합니다. ### 보안 원칙 1. **다층 방어 (Defense in Depth)**: FormRequest → Service → Repository → 프론트엔드, 각 계층에서 독립적 검증 2. **최소 권한 원칙 (Principle of Least Privilege)**: 필요한 최소한의 권한만 부여 3. **화이트리스트 방식 (Whitelist Approach)**: 명시적으로 허용된 것만 통과 4. **검증 우선 (Fail Secure)**: 검증 실패 시 안전한 상태로 복귀 5. **확장 가능한 검증 (Extensible Validation)**: Hook을 통해 모듈/플러그인이 검증 규칙을 동적으로 추가 가능 --- ## 보안 아키텍처 ### 전체 검증 플로우 ```text 사용자 요청 ↓ 1. Controller 진입 전 검증 (FormRequest + 10개 Custom Rule) ├─ ValidLayoutStructure: JSON 스키마 + 중첩 깊이 + actions + permissions 검증 ├─ ComponentExists: 컴포넌트 매니페스트 대조 (3카테고리, 1시간 캐싱) ├─ WhitelistedEndpoint: API 엔드포인트 화이트리스트 + 경로 트래버설 차단 ├─ NoExternalUrls: 7개 위험 URI 스킴 + 프로토콜 상대 URL 차단 ├─ ValidParentLayout: 상속 순환 참조 감지 + 깊이 10 제한 ├─ ValidSlotStructure: 부모에서 정의된 슬롯만 허용 ├─ ValidDataSourceMerge: 상속 체인 전체 데이터소스 ID 고유성 ├─ ValidPermissionStructure: or/and 구조 + 깊이 3 제한 + 정규식 형식 ├─ SafeTemplatePath: 13개 Path Traversal 패턴 + 5회 URL 디코딩 + NULL 바이트 └─ AllowedTemplateFileType: 14개 확장자 화이트리스트 ↓ 2. Hook 기반 확장 검증 (모듈/플러그인이 추가한 규칙) ↓ 3. Service 계층 (비즈니스 로직) ├─ 레이아웃 상속 병합 ├─ 훅 실행 (before_save, after_save) └─ 데이터베이스 저장 ↓ 4. 프론트엔드 렌더링 ├─ React 자동 이스케이프 (XSS 방지) ├─ CSRF 토큰 검증 (Laravel Sanctum Bearer 토큰) └─ Eloquent ORM 파라미터 바인딩 (SQL Injection 방지) ``` ### 보안 계층 구조 ```text ┌─────────────────────────────────────────────────────────────┐ │ 사용자 브라우저 │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ React 렌더링 (자동 이스케이프) │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↕ HTTPS ┌─────────────────────────────────────────────────────────────┐ │ Laravel Backend │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ 1. FormRequest 검증 (Controller 진입 전) │ │ │ │ 10개 Custom Rule + Hook 확장 규칙 │ │ │ └──────────────────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ 2. Service 계층 (비즈니스 로직) │ │ │ │ 레이아웃 상속 병합 + 훅 실행 │ │ │ └──────────────────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ 3. Repository 계층 (데이터 접근) │ │ │ │ Eloquent ORM (파라미터 바인딩) │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↕ ┌─────────────────────────────────────────────────────────────┐ │ MySQL Database │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 검증 계층 ### 1단계: FormRequest 검증 (Controller 진입 전) **위치**: `app/Http/Requests/Layout/` **목적**: Controller에 도달하기 전에 모든 악의적 입력을 차단 G7은 **용도별 7개 FormRequest**로 검증을 분리합니다. 각 FormRequest는 용도에 맞는 Custom Rule 조합을 적용하며, Hook을 통해 모듈/플러그인이 규칙을 확장할 수 있습니다. > 상세: [FormRequest별 Rule 적용 현황](#formrequest별-rule-적용-현황) ### 2단계: Service 계층 (비즈니스 로직) **위치**: `app/Services/LayoutService.php` **특징**: - 검증 로직 없음 (FormRequest에서 완료) - 순수 비즈니스 로직만 처리 - 훅 시스템을 통한 확장성 (`before_save`, `after_save`) ### 3단계: Repository 계층 (데이터 접근) **위치**: `app/Repositories/LayoutRepository.php` **특징**: - Eloquent ORM 사용 (파라미터 바인딩으로 SQL Injection 자동 방어) - 직접 SQL 문자열 연결 금지 --- ## Custom Validation Rules G7은 레이아웃 JSON 보안을 위해 **10개의 Custom Validation Rule**을 제공합니다. ### 1. ValidLayoutStructure **파일**: `app/Rules/ValidLayoutStructure.php` **목적**: 레이아웃 JSON의 구조적 유효성 검증 **검증 항목**: - 필수 필드 존재 확인 (`version`, `layout_name`, `components`) - `extends` 레이아웃: `components` 또는 `slots` 중 하나 필수 - 재귀적 컴포넌트 구조 검증 (type, name 필수) - 컴포넌트 `type`: `basic`, `composite`, `layout` 중 하나만 허용 - **최대 중첩 깊이 제한: 10단계** (`MAX_DEPTH = 10`) - `actions` 배열 구조 검증 (`type` 또는 `event` 중 하나 필수) - `permissions` 필드: `ValidPermissionStructure` Rule에 위임 - 슬롯 참조(`{ "slot": "name" }`)와 Partial 참조(`{ "partial": "path" }`) 허용 ### 2. ComponentExists **파일**: `app/Rules/ComponentExists.php` **목적**: 존재하지 않는 컴포넌트 참조 방지 **검증 방법**: 1. 템플릿의 `components.json` 매니페스트 로드 2. **3개 카테고리**: `basic`, `composite`, `layout` 컴포넌트를 Set으로 구축 3. 레이아웃 JSON의 모든 `name` 필드를 매니페스트와 대조 4. 재귀적으로 `children` 배열 검증 **캐싱 전략**: - 캐시 키: `template.{template_id}.components_manifest` - **캐시 TTL: 1시간** (`CACHE_TTL = 3600`) ### 3. WhitelistedEndpoint **파일**: `app/Rules/WhitelistedEndpoint.php` **목적**: API 엔드포인트 화이트리스트 검증 **허용 패턴**: ```regex ^/api/((admin|auth|public)/|(modules|plugins)/{vendor-id}/) ``` `/api/modules/{vendor-module}/`, `/api/plugins/{vendor-plugin}/` 은 코어 라우트 프로바이더가 등록하는 확장 API 네임스페이스로, 확장 식별자(vendor-name — 하이픈 포함) 세그먼트가 뒤따를 때만 허용된다. **검증 대상**: - `data_sources[].endpoint` — 데이터소스 엔드포인트 - `components[].actions[].endpoint` — 액션 핸들러 엔드포인트 (재귀적 children 포함) **차단**: - 외부 URL (`http://`, `https://`) - 비공개 API (`/api/internal/*`) - 확장 식별자 형식이 아닌 modules/plugins 세그먼트 (`/api/modules/assets/*` 등) - 직접 경로 (`/admin/*`) - **경로 트래버설**: `../`, `..\` 패턴 차단 ### 4. NoExternalUrls **파일**: `app/Rules/NoExternalUrls.php` **목적**: 외부 URL 차단으로 데이터 유출 및 악의적 리소스 로딩 방지 **차단 대상 — 7개 위험 URI 스킴**: | 스킴 | 위험성 | |------|--------| | `http://` | 외부 서버 통신, 데이터 유출 | | `https://` | 외부 서버 통신, 데이터 유출 | | `data:` | 인라인 데이터 삽입, XSS 벡터 | | `javascript:` | 임의 JavaScript 실행 | | `vbscript:` | 임의 VBScript 실행 (IE) | | `file:` | 로컬 파일 시스템 접근 | | `ftp:` | 외부 FTP 서버 통신 | **추가 차단**: `//`로 시작하는 프로토콜 상대 URL **외부의 기준**: 사이트 자기 host(`app.url`)와 운영자가 「공개 자산 스토리지」로 선언한 디스크의 host 는 외부가 아닙니다. 레이아웃 첨부 API 가 스스로 발급하는 주소(공개 서빙 URL·직접 URL)가 그 host 를 쓰므로, 여기서 차단하면 image 위젯으로 올린 파일이 업로드는 되고 저장은 422 가 됩니다. 판정은 접두 문자열 비교가 아니라 브라우저와 같은 정규화 뒤의 host 등가 비교입니다(`App\Support\SiteAssetHosts`). `host.evil.com`·`host@evil.com`·`evil.com\@host` 같은 흉내 host 는 통과하지 않으며, 요청의 `Host` 헤더는 위조 가능하므로 근거로 쓰지 않습니다. 프로토콜 상대 URL 과 http/https 밖의 스킴은 host 가 같아도 차단됩니다. **검증 범위**: `components[]` → `props`, `actions`, `lifecycle`, `onComponentEvent`, `slots`, `component_layout`, `responsive` 와 최상위 `init_actions`/`modals`/`named_actions`/`errorHandling` 내 모든 문자열 값을 재귀적으로 스캔. `style` 은 스캔 대상이 아닙니다 — 배경 이미지(`backgroundImage`)가 절대 URL 이어도 통과하는 이유입니다. ### 5. ValidParentLayout **파일**: `app/Rules/ValidParentLayout.php` **목적**: 레이아웃 상속 체계의 안전성 보장 **검증 항목**: 1. **부모 존재 확인**: DB에서 `template_id` + `name` 조회 2. **순환 참조 감지**: 재귀적 체인 추적 (`visited` 배열로 방문 노드 기록) 3. **상속 깊이 제한**: `MAX_INHERITANCE_DEPTH = 10` > 상세: [레이아웃 상속 보안](#레이아웃-상속-보안) ### 6. ValidSlotStructure **파일**: `app/Rules/ValidSlotStructure.php` **목적**: 부모 레이아웃에서 정의된 슬롯만 허용 **검증 로직**: 1. `extends` 필드가 없으면 검증 통과 2. 부모 레이아웃의 `components`에서 `{ "slot": "name" }` 패턴을 재귀적으로 수집 3. 자식 레이아웃의 `slots` 키가 부모에 정의된 슬롯 이름과 일치하는지 검증 4. **부모에 없는 슬롯 이름 사용 시 차단** ### 7. ValidDataSourceMerge **파일**: `app/Rules/ValidDataSourceMerge.php` **목적**: 상속 체인 전체에서 데이터소스 ID 고유성 보장 **검증 로직**: 1. 현재 레이아웃 내 `data_sources[].id` 중복 검사 2. `extends`가 있으면 **부모 체인 전체를 순회**하여 모든 데이터소스 ID 수집 3. 현재 레이아웃 ID와 부모 체인 ID의 교집합 검사 → **중복 시 차단** 4. 무한 루프 방지 (`visited` 배열) ### 8. ValidPermissionStructure **파일**: `app/Rules/ValidPermissionStructure.php` **목적**: 권한 구조의 형식적 유효성 검증 **지원 구조**: - **Flat array**: `["perm.read", "perm.write"]` - **OR 구조**: `{ "or": ["perm.read", "perm.write"] }` (하나라도 만족) - **AND 구조**: `{ "and": ["perm.read", "perm.write"] }` (모두 만족) - **중첩 구조**: `{ "or": ["perm.a", { "and": ["perm.b", "perm.c"] }] }` **제한**: - **최대 중첩 깊이: 3단계** (`MAX_DEPTH = 3`) - or/and 연산자에 **최소 2개 항목** 필수 (`MIN_OPERATOR_ITEMS = 2`) - 권한 식별자 정규식: `/^[a-z0-9_-]+\.[a-z0-9_-]+(\.[a-z0-9_-]+)*$/i` **사용 위치**: 레이아웃 최상위 `permissions` + 컴포넌트 레벨 `permissions` (ValidLayoutStructure에서 위임) ### 9. SafeTemplatePath **파일**: `app/Rules/SafeTemplatePath.php` **목적**: 템플릿 파일 경로의 Path Traversal 공격 방지 > 상세: [경로 보안 (Path Traversal 방어)](#경로-보안-path-traversal-방어) ### 10. AllowedTemplateFileType **파일**: `app/Rules/AllowedTemplateFileType.php` **목적**: 허용된 파일 확장자만 통과 **14개 허용 확장자**: | 카테고리 | 확장자 | |---------|--------| | Scripts | `js`, `mjs` | | Styles | `css` | | Data | `json` | | Images | `png`, `jpg`, `jpeg`, `svg`, `webp`, `gif` | | Fonts | `woff`, `woff2`, `ttf`, `otf`, `eot` | **그 외 모든 확장자 차단** (PHP, HTML, EXE 등) --- ## FormRequest별 Rule 적용 현황 G7은 레이아웃 CRUD 작업별로 **7개 FormRequest**를 운용합니다. 각 FormRequest는 용도에 맞는 Rule 조합을 적용합니다. ### 비교 표 | FormRequest | 용도 | VLS | CE | WE | NEU | VPL | VSS | VDM | VPS | |-------------|------|:---:|:--:|:--:|:---:|:---:|:---:|:---:|:---:| | **StoreLayoutRequest** | 새 레이아웃 생성 | ✅ | ✅ | ✅ | ✅ | | | | | | **UpdateLayoutRequest** | 메타+콘텐츠 수정 | ✅* | ✅* | ✅* | ✅* | | | | | | **UpdateLayoutContentRequest** | 콘텐츠만 수정 | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | **StoreLayoutInheritanceRequest** | 상속 레이아웃 생성 | | | | | ✅ | ✅ | ✅ | | | **UpdateLayoutInheritanceRequest** | 상속 레이아웃 수정 | | | | | ✅ | ✅ | ✅ | | | **StoreLayoutPreviewRequest** | 미리보기 | | | | | | | | | | **GetLayoutRequest** | 레이아웃 조회 | | | | | | | | | **범례**: VLS=ValidLayoutStructure, CE=ComponentExists, WE=WhitelistedEndpoint, NEU=NoExternalUrls, VPL=ValidParentLayout, VSS=ValidSlotStructure, VDM=ValidDataSourceMerge, VPS=ValidPermissionStructure **✅\*** = `sometimes` 규칙 (필드가 존재할 때만 적용) ### 핵심 포인트 - **StoreLayoutRequest / UpdateLayoutRequest**: 기본 4개 Rule (구조, 컴포넌트, 엔드포인트, URL) - **UpdateLayoutContentRequest**: 가장 엄격 — 상속 관련 4개 Rule 추가 (총 8개) - **StoreLayoutInheritanceRequest / UpdateLayoutInheritanceRequest**: 상속 전용 3개 Rule - **StoreLayoutPreviewRequest**: 최소 검증 (`content: required, array`) - **GetLayoutRequest**: 조회 전용 (보안 Rule 없음) --- ## 레이아웃 상속 보안 G7 레이아웃은 `extends`/`slots`/`partial` 시스템으로 상속을 지원합니다. 상속 체계의 안전성은 3개 Rule이 협력하여 보장합니다. ### 순환 참조 방지 (ValidParentLayout) ```text A extends B → B extends C → C extends A ← 차단! ``` **메커니즘**: 1. `extends` 필드의 부모 레이아웃을 DB에서 조회 2. 부모의 `extends`를 재귀적으로 추적 (`visited` 배열로 방문 기록) 3. 이미 방문한 레이아웃 또는 자기 자신을 참조하면 순환 참조로 판정 4. **최대 상속 깊이: 10단계** — 초과 시 차단 ### 슬롯 안전성 (ValidSlotStructure) ```text 부모 레이아웃에 { "slot": "header" }, { "slot": "content" } 정의 ↓ 자식 레이아웃에서 slots: { "header": [...], "footer": [...] } ↑ 차단! (부모에 "footer" 슬롯 미정의) ``` - 부모 `components` 트리를 재귀 탐색하여 `{ "slot": "name" }` 패턴 수집 - 자식의 `slots` 키가 부모 슬롯 목록에 포함되는지 검증 ### 데이터소스 ID 고유성 (ValidDataSourceMerge) ```text 조부모: data_sources: [{ id: "users" }] 부모: data_sources: [{ id: "posts" }] 자식: data_sources: [{ id: "users" }] ← 차단! (조부모와 ID 충돌) ``` - **상속 체인 전체**를 순회하여 모든 데이터소스 ID 수집 - 현재 레이아웃 ID와의 교집합 검사 - 무한 루프 방지 로직 포함 --- ## 경로 보안 (Path Traversal 방어) **파일**: `app/Rules/SafeTemplatePath.php` G7은 OS 레벨 파일 권한에 의존하지 않고, **애플리케이션 레벨에서 경로를 적극적으로 검증**합니다. ### 13개 Path Traversal 패턴 차단 | 패턴 | 설명 | |------|------| | `../` | Unix 상위 디렉토리 | | `..\` | Windows 상위 디렉토리 | | `//` | 이중 슬래시 | | `%2e%2e%2f` | URL 인코딩 `../` | | `%2e%2e/` | 부분 URL 인코딩 | | `%2e%2e%5c` | URL 인코딩 `..\` | | `%2e%2e\` | 부분 URL 인코딩 | | `..%2f` | 혼합 인코딩 `../` | | `..%5c` | 혼합 인코딩 `..\` | | `.%2e/` | 부분 인코딩 `../` | | `.%2e\` | 부분 인코딩 `..\` | ### 다중 인코딩 공격 방지 ```php // 최대 5회 반복 URL 디코딩 (이중/삼중 인코딩 공격 방지) for ($i = 0; $i < 5 && $decodedPath !== $previousPath; $i++) { $previousPath = $decodedPath; $decodedPath = urldecode($decodedPath); } ``` 공격자가 `%252e%252e%252f` (삼중 인코딩)를 사용해도 5회 디코딩으로 원본 패턴이 드러남 ### 추가 방어 | 방어 항목 | 설명 | |----------|------| | **절대 경로 차단** | `/`, `\`, `C:` 등 절대 경로 시작 패턴 차단 (Windows/Linux 모두) | | **NULL 바이트 차단** | `\0` 문자 감지 시 차단 (C 언어 문자열 종료 공격) | | **basePath 외부 접근 차단** | `realpath()` 정규화 후 `basePath` 외부 경로 감지 시 차단 | --- ## Hook 기반 검증 확장 모듈/플러그인은 **6개 Filter Hook**을 통해 레이아웃 검증 규칙을 동적으로 추가할 수 있습니다. ### 사용 가능한 Hook | Hook 이름 | FormRequest | 용도 | |----------|-------------|------| | `core.layout.store_validation_rules` | StoreLayoutRequest | 레이아웃 생성 시 규칙 추가 | | `core.layout.update_validation_rules` | UpdateLayoutRequest | 레이아웃 수정 시 규칙 추가 | | `core.layout.update_content_validation_rules` | UpdateLayoutContentRequest | 콘텐츠 수정 시 규칙 추가 | | `core.layout.store_inheritance_validation_rules` | StoreLayoutInheritanceRequest | 상속 레이아웃 생성 시 규칙 추가 | | `core.layout.update_inheritance_validation_rules` | UpdateLayoutInheritanceRequest | 상속 레이아웃 수정 시 규칙 추가 | | `core.layout.get_validation_rules` | GetLayoutRequest | 레이아웃 조회 시 규칙 추가 | ### 사용 예시 ```php // 모듈의 Listener에서 커스텀 검증 규칙 추가 class LayoutValidationListener { public function handle(array $rules, StoreLayoutRequest $request): array { // 모듈 전용 검증 규칙 추가 $rules['content'][] = new MyCustomRule(); return $rules; } } ``` ```php // 모듈의 ServiceProvider에서 Hook 등록 HookManager::addFilter( 'core.layout.store_validation_rules', [LayoutValidationListener::class, 'handle'], priority: 10 ); ``` > 참고: Filter 훅이므로 리스너에서 `type: 'filter'` 명시 필수 (미지정 시 반환값 무시) > 상세: [훅 시스템](../extension/hooks.md) --- ## 공격 방어 전략 ### 공격 시나리오별 방어 매핑 | 공격 유형 | 공격 벡터 | 방어 메커니즘 | 차단 계층 | |---------|---------|------------|---------| | **XSS** | `" } } ] } ``` **결과**: FormRequest의 첫 번째 검증 규칙에서 차단 (422 응답) - `WhitelistedEndpoint`: `/api/internal/` 차단 - `ComponentExists`: `NonExistentComponent` 차단 - `NoExternalUrls`: `https://evil.com` 차단 - React: `