편집기 헤더 「로고 이미지」에 파일을 올린 뒤 저장하면 「HTTPS 프로토콜 URL은 허용되지 않습니다」 422 로 거부되던 결함. 프록시 서빙 URL 이 절대 형태로 발급되어 NoExternalUrls 가 서버 자신의 주소를 외부로 차단했고, 배경 이미지는 style 로 들어가 스캔 밖이어서 로고(propValue)에서만 드러났다. - proxyUrl 을 사이트 상대 경로로 발급 (발급 시점 도메인·스킴에 묶이지 않는다) - SiteAssetHosts 신설: app.url host + 운영자가 선언한 공개 자산 디스크 host 를 브라우저와 같은 정규화 뒤 등가 비교. 요청 Host 헤더는 근거로 쓰지 않고, 흉내 host·protocol-relative·위험 스킴은 종전대로 차단 - 회귀 테스트 4클래스(71 passed) + 시나리오 매니페스트 + Playwright 2케이스, 공개 문서(SECURITY·security·storage-driver·api)와 규정 표 갱신 함께 담긴 릴리즈 정비: - 7.0.11 미출시 섹션의 릴리즈 날짜를 09-09 로 갱신 (코어·board·page·admin_basic·basic·ja 언어팩 3종) - 배포본에 실리는 주석·독블록·시나리오의 내부 호칭을 중립 표현으로 정리하고, 동봉 vendor 자산의 우연한 토큰은 누출 검사에서 경로 규칙으로만 제외 - 버전 bump 검사: github_url 없는 번들을 언어팩과 같은 origin/release 페이로드 기준선으로 판정(상시 경고 제거), 코어 버전 판독이 env 래핑을 읽도록 보정
621 lines
27 KiB
Markdown
621 lines
27 KiB
Markdown
# 그누보드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** | `<script>` 태그 삽입 | React 자동 이스케이프 | 프론트엔드 |
|
|
| **SQL Injection** | `'; DROP TABLE--` | Eloquent ORM 파라미터 바인딩 | Repository |
|
|
| **CSRF** | 토큰 없는 요청 | Laravel Sanctum Bearer 토큰 | 미들웨어 |
|
|
| **경로 트래버설** | `../../etc/passwd` | SafeTemplatePath (13패턴 + 5회 디코딩) | FormRequest |
|
|
| **다중 인코딩 경로 트래버설** | `%252e%252e%252f` | SafeTemplatePath 5회 반복 디코딩 | FormRequest |
|
|
| **NULL 바이트 경로 공격** | `file.php\0.jpg` | SafeTemplatePath NULL 바이트 감지 | FormRequest |
|
|
| **외부 리소스 로딩** | `https://evil.com` | NoExternalUrls (7개 스킴) | FormRequest |
|
|
| **XSS via URI** | `javascript:alert(1)` | NoExternalUrls (`javascript:` 차단) | FormRequest |
|
|
| **Data URI 삽입** | `data:text/html,...` | NoExternalUrls (`data:` 차단) | FormRequest |
|
|
| **프로토콜 상대 URL** | `//evil.com/malicious.js` | NoExternalUrls (`//` 차단) | FormRequest |
|
|
| **허용되지 않은 API** | `/api/internal/` | WhitelistedEndpoint | FormRequest |
|
|
| **API 경로 트래버설** | `/api/admin/../internal/` | WhitelistedEndpoint (`../` 차단) | FormRequest |
|
|
| **존재하지 않는 컴포넌트** | `MaliciousComponent` | ComponentExists (매니페스트 대조) | FormRequest |
|
|
| **DoS (깊은 중첩)** | 11단계 이상 | ValidLayoutStructure (`MAX_DEPTH = 10`) | FormRequest |
|
|
| **DoS (상속 깊이)** | 11단계 이상 | ValidParentLayout (`MAX_INHERITANCE_DEPTH = 10`) | FormRequest |
|
|
| **DoS (권한 중첩)** | 4단계 이상 | ValidPermissionStructure (`MAX_DEPTH = 3`) | FormRequest |
|
|
| **상속 순환 참조** | A→B→C→A | ValidParentLayout (visited 추적) | FormRequest |
|
|
| **유령 슬롯 주입** | 부모에 없는 슬롯 | ValidSlotStructure | FormRequest |
|
|
| **데이터소스 ID 충돌** | 부모와 동일 ID | ValidDataSourceMerge (체인 전체 검사) | FormRequest |
|
|
| **위험 파일 업로드** | `malicious.php` | AllowedTemplateFileType (14개 화이트리스트) | FormRequest |
|
|
|
|
### 복합 공격 방어
|
|
|
|
악의적 사용자가 여러 공격 기법을 조합하더라도, 각 계층에서 독립적으로 검증하므로 하나의 공격이 통과하더라도 다음 계층에서 차단됩니다.
|
|
|
|
```json
|
|
{
|
|
"version": "1.0.0",
|
|
"layout_name": "combined_attack",
|
|
"data_sources": [
|
|
{
|
|
"endpoint": "/api/internal/secrets",
|
|
"params": { "filter": "'; DROP TABLE--" }
|
|
}
|
|
],
|
|
"components": [
|
|
{
|
|
"name": "NonExistentComponent",
|
|
"type": "basic",
|
|
"props": {
|
|
"imageUrl": "https://evil.com/image.jpg",
|
|
"onClick": "<script>alert(1)</script>"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**결과**: FormRequest의 첫 번째 검증 규칙에서 차단 (422 응답)
|
|
- `WhitelistedEndpoint`: `/api/internal/` 차단
|
|
- `ComponentExists`: `NonExistentComponent` 차단
|
|
- `NoExternalUrls`: `https://evil.com` 차단
|
|
- React: `<script>` 태그는 자동 이스케이프 (텍스트로 표시)
|
|
- Eloquent ORM: SQL 구문은 파라미터 바인딩으로 안전 처리
|
|
|
|
---
|
|
|
|
## 보안 모범 사례
|
|
|
|
### 템플릿 개발자를 위한 지침
|
|
|
|
1. **컴포넌트 명세 문서화**: `components.json`에 모든 컴포넌트를 `basic`/`composite`/`layout` 카테고리로 분류 등록
|
|
2. **외부 의존성 최소화**: 외부 CDN 사용 자제, `/public/build/template/` 디렉토리에 번들링
|
|
3. **props 타입 검증**: 컴포넌트 내부에서 TypeScript로 props 타입 검증
|
|
|
|
### 확장 개발자를 위한 지침
|
|
|
|
1. **Hook을 통한 검증 추가**: 모듈 전용 검증이 필요하면 `core.layout.*_validation_rules` Hook 사용
|
|
2. **Custom Rule 작성**: `Illuminate\Contracts\Validation\ValidationRule` 인터페이스 구현
|
|
3. **`__()` 함수 사용**: Custom Rule 에러 메시지는 반드시 다국어 처리
|
|
|
|
### 시스템 관리자를 위한 지침
|
|
|
|
1. **정기적인 보안 감사**: `MaliciousJsonTest.php` 실행으로 방어 체계 검증
|
|
2. **로그 모니터링**: 422 응답 빈도 모니터링, 반복적인 악의적 시도 IP 차단
|
|
3. **환경 변수 보호**: `.env` 파일 권한 제한 (600), API 키/DB 비밀번호 안전 보관
|
|
4. **업데이트 관리**: Laravel 및 의존성 패키지 최신 버전 유지
|
|
|
|
### 코드 리뷰 체크리스트
|
|
|
|
- [ ] 모든 검증 로직이 FormRequest에 있는가?
|
|
- [ ] Service에 검증 로직이 없는가?
|
|
- [ ] Custom Rule에서 `__()` 함수로 다국어 처리했는가?
|
|
- [ ] 외부 URL 참조가 없는가?
|
|
- [ ] `dangerouslySetInnerHTML` 사용이 없는가?
|
|
- [ ] DB 쿼리가 Eloquent ORM을 사용하는가?
|
|
- [ ] API 라우트에 인증 미들웨어가 있는가?
|
|
- [ ] 새로운 엔드포인트가 WhitelistedEndpoint 패턴에 부합하는가?
|
|
- [ ] 레이아웃 상속 시 부모 슬롯과 데이터소스 ID 충돌이 없는가?
|
|
|
|
---
|
|
|
|
## 관련 문서
|
|
|
|
### 보안 관련
|
|
|
|
- [프론트엔드 보안 및 검증](frontend/security.md) — XSS, 표현식, 상태 노출, 인증, 에셋 보안
|
|
- [템플릿 보안 정책](extension/template-security.md) — 템플릿 에셋 서빙, 경로 보안
|
|
|
|
### 백엔드
|
|
|
|
- [검증 (Validation)](backend/validation.md) — FormRequest + Custom Rule 패턴
|
|
- [인증 및 세션 처리](backend/authentication.md) — Sanctum 토큰 기반 인증
|
|
- [Service-Repository 패턴](backend/service-repository.md) — 계층 분리
|
|
|
|
### 확장 시스템
|
|
|
|
- [훅 시스템](extension/hooks.md) — Action/Filter Hook, 모듈/플러그인 확장
|
|
- [레이아웃 확장 시스템](extension/layout-extensions.md) — 레이아웃 동적 주입
|
|
|
|
### 프론트엔드
|
|
|
|
- [레이아웃 JSON 스키마](frontend/layout-json.md) — JSON 구조 명세
|
|
- [레이아웃 JSON - 상속](frontend/layout-json-inheritance.md) — extends/slots/partial 시스템
|
|
- [인증 시스템 (AuthManager)](frontend/auth-system.md) — 프론트엔드 인증 흐름
|
|
|
|
### 참고 파일
|
|
|
|
- **Custom Rules**: `app/Rules/*.php` (10개 보안 관련)
|
|
- **FormRequests**: `app/Http/Requests/Layout/*.php` (7개)
|
|
- **통합 테스트**: `tests/Feature/Security/MaliciousJsonTest.php`
|
|
- **다국어 파일**: `lang/ko/validation.php`, `lang/en/validation.php`
|
|
|
|
### 외부 문서
|
|
|
|
- [Laravel 보안 가이드](https://laravel.com/docs/12.x/authentication)
|
|
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
|
|
|
---
|
|
|
|
**마지막 업데이트**: 2026-03-30
|