# 템플릿 보안 정책 > **목적**: 템플릿 시스템의 보안 정책 및 API 서빙 규칙 가이드 --- ## 목차 1. [레이아웃 JSON 검증](#1-레이아웃-json-검증) 2. [의존성 검증](#2-의존성-검증) 3. [XSS 방지](#3-xss-방지) 4. [템플릿 API 서빙 규칙](#4-템플릿-api-서빙-규칙) 5. [파일 확장자 화이트리스트](#5-파일-확장자-화이트리스트) --- ## 1. 레이아웃 JSON 검증 ### 검증 계층 ``` FormRequest (StoreLayoutRequest) ↓ Custom Rule: ValidLayoutStructure ↓ Custom Rule: WhitelistedEndpoint ↓ Custom Rule: NoExternalUrls ↓ Custom Rule: ComponentExists ``` ### 검증 항목 | 항목 | 설명 | |------|------| | JSON 구조 유효성 | 올바른 JSON 형식인지 검증 | | 최대 중첩 깊이 | 10단계 제한 | | 엔드포인트 화이트리스트 | `/api/(admin\|auth\|public)/` 패턴만 허용 | | 외부 URL 금지 | 외부 도메인 URL 차단 | | 컴포넌트 존재 여부 | components.json 기준으로 검증 | --- ## 2. 의존성 검증 ### 템플릿 설치 시 체크 - 필수 의존성 템플릿 설치 여부 - 순환 의존성 방지 - 버전 호환성 (Semantic Versioning) ### 예시 ```json // template.json { "dependencies": { "sirsoft-component-library": "^1.0.0" } } ``` --- ## 3. XSS 방지 ### 데이터 바인딩 - 모든 `{{data.field}}` 값은 자동 이스케이프 - `dangerouslySetInnerHTML` 사용 금지 - HTML 삽입 시 서버 검증 필수 ### Translation - `$t:key` 값은 다국어 파일에서만 로드 - 사용자 입력 키 금지 --- ## 4. 템플릿 API 서빙 규칙 ``` 중요: 템플릿 에셋은 API를 통해 제공 ✅ 필수: 파일 복사 대신 API 엔드포인트 사용 ``` ### 원칙 - 템플릿 설치 시 빌드된 파일(`/templates/[vendor-template]/dist/`)은 그대로 유지 - `/public/build/templates/` 디렉토리에 파일 복사하지 않음 - API 엔드포인트를 통해 동적으로 템플릿 에셋 제공 - 보안: 활성화된 템플릿의 에셋만 접근 가능 ### API 엔드포인트 ```php // routes/api.php Route::get('/templates/{identifier}/assets/{file}', [TemplateAssetController::class, 'serve']) ->where('file', '.*') ->name('api.templates.assets'); ``` ### Controller 구현 (TemplateAssetController) ```php templateService->isTemplateActive($identifier)) { return $this->notFound('messages.template.not_active'); } // 2. 파일 경로 검증 (보안) $allowedExtensions = ['js', 'js.map', 'css', 'css.map', 'woff', 'woff2', 'ttf', 'eot', 'svg', 'png', 'jpg', 'jpeg', 'gif']; $extension = pathinfo($file, PATHINFO_EXTENSION); if (!in_array($extension, $allowedExtensions)) { return $this->forbidden('messages.template.invalid_file_type'); } // 3. 실제 파일 경로 생성 $basePath = base_path("templates/{$identifier}"); $filePath = realpath("{$basePath}/{$file}"); // 4. 디렉토리 탈출 방지 if (!$filePath || !str_starts_with($filePath, $basePath)) { return $this->forbidden('messages.template.invalid_path'); } // 5. 파일 존재 확인 if (!file_exists($filePath) || !is_file($filePath)) { return $this->notFound('messages.template.file_not_found'); } // 6. MIME 타입 설정 $mimeTypes = [ 'js' => 'application/javascript', 'css' => 'text/css', 'map' => 'application/json', 'woff' => 'font/woff', 'woff2' => 'font/woff2', 'ttf' => 'font/ttf', 'eot' => 'application/vnd.ms-fontobject', 'svg' => 'image/svg+xml', 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', ]; $mimeType = $mimeTypes[$extension] ?? 'application/octet-stream'; // 7. 캐싱 헤더 추가 return response()->file($filePath, [ 'Content-Type' => $mimeType, 'Cache-Control' => 'public, max-age=31536000, immutable', 'Access-Control-Allow-Origin' => '*', ]); } } ``` ### Service 메서드 ```php Template::where('identifier', $identifier) ->where('is_active', true) ->exists() ); } } ``` ### Blade 사용 예시 ```php
@if($activeTemplate) @endif ``` ### 보안 규칙 | 규칙 | 설명 | |------|------| | ✅ 활성화된 템플릿만 접근 가능 | 비활성 템플릿 에셋 접근 차단 | | ✅ 화이트리스트 기반 확장자 검증 | 허용된 확장자만 제공 | | ✅ 디렉토리 탈출 공격 방지 | `realpath()`, `str_starts_with()` 사용 | | ✅ MIME 타입 명시적 설정 | Content-Type 헤더 지정 | | ✅ 적절한 캐싱 헤더 | `Cache-Control`, `immutable` | | ❌ 비활성 템플릿 에셋 접근 금지 | 활성화 여부 검증 필수 | | ❌ 임의의 파일 경로 접근 금지 | 경로 검증 필수 | ### 장점 1. **보안 강화**: 활성화된 템플릿만 접근 가능 2. **디스크 공간 절약**: 파일 복사 불필요 3. **동적 제어**: 템플릿 비활성화 시 즉시 접근 차단 4. **캐싱 최적화**: HTTP 캐싱으로 성능 유지 5. **유지보수 용이**: 단일 소스(dist 디렉토리)만 관리 --- ## 5. 파일 확장자 화이트리스트 ``` 중요: 템플릿 에셋 파일 확장자 제한 ✅ 필수: 화이트리스트 기반 확장자 검증 ``` ### 허용 확장자 목록 #### 스크립트 파일 | 확장자 | 설명 | |--------|------| | `js` | JavaScript | | `mjs` | ES Module JavaScript | #### 스타일 파일 | 확장자 | 설명 | |--------|------| | `css` | Cascading Style Sheets | #### 소스맵 (`map`) — 로컬 개발 환경 전용 소스맵에는 원본 TS/TSX 전문(`sourcesContent`)이 담기고, 이 확장자 화이트리스트가 확장 에셋 서빙의 유일한 방어선이다. 따라서 `map` 은 `local` 환경에서만 허용 목록에 덧붙고, 그 외 환경에서는 어떤 경우에도 서빙하지 않는다. 로컬에서 허용하는 이유는 dev 빌드 산출물의 `//# sourceMappingURL` 이 개별 에셋 서빙 URL 을 가리키기 때문이다 — 차단하면 브라우저 콘솔에 404 가 쌓인다. `--production` 빌드는 소스맵을 생성하지 않으므로(`G7_BUILD_SOURCEMAP=0`) 운영에서는 참조 자체가 없다. 이 주입이 실제로 먹으려면 확장의 vite config 가 그 환경변수를 읽어야 한다. `sourcemap: true` 리터럴 하드코딩은 주입을 조용히 무시해 `--production` 빌드에도 `.map` 과 dangling 참조가 배포본에 남는다. 확장 vite config 의 소스맵 선언은 다음 형태만 사용한다: ```ts sourcemap: !['0', 'false'].includes(process.env.G7_BUILD_SOURCEMAP ?? ''), ``` 판정 지점은 `Allowed{Module,Plugin,Template}FileType::getAllowedExtensions()` 한 곳이며, `validate()` 도 이 게터를 참조한다(상수 직접 참조 금지 — 환경 분기가 우회된다). `public/build/**` 는 docroot 라 이 화이트리스트를 거치지 않는다. `--production` 재빌드는 기존 `.map` 을 지우지 않으므로 직접 삭제해야 한다. #### 폰트 파일 | 확장자 | 설명 | |--------|------| | `woff` | Web Open Font Format | | `woff2` | Web Open Font Format 2 | | `ttf` | TrueType Font | | `otf` | OpenType Font | | `eot` | Embedded OpenType | #### 이미지 파일 | 확장자 | 설명 | |--------|------| | `png` | Portable Network Graphics | | `jpg` | JPEG Image | | `jpeg` | JPEG Image | | `svg` | Scalable Vector Graphics | | `webp` | WebP Image | | `gif` | Graphics Interchange Format | #### 데이터 파일 | 확장자 | 설명 | |--------|------| | `json` | JSON 데이터 (components.json, template.json 등) | ### TemplateService 구현 ```php // app/Services/TemplateService.php /** * 허용된 파일 확장자 화이트리스트 */ private const ALLOWED_EXTENSIONS = [ 'js', 'mjs', 'css', 'json', 'png', 'jpg', 'jpeg', 'svg', 'webp', 'gif', 'woff', 'woff2', 'ttf', 'otf', 'eot', ]; /** * 파일 확장자 검증 */ private function validateFileExtension(string $filePath): bool { $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); // .js.map, .css.map 처리 if (str_ends_with($filePath, '.map')) { $extension = 'map'; } return in_array($extension, self::ALLOWED_EXTENSIONS); } /** * MIME 타입 매핑 */ private function getMimeType(string $extension): string { $mimeTypes = [ 'js' => 'application/javascript', 'mjs' => 'application/javascript', 'css' => 'text/css', 'json' => 'application/json', 'map' => 'application/json', 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'svg' => 'image/svg+xml', 'webp' => 'image/webp', 'gif' => 'image/gif', 'woff' => 'font/woff', 'woff2' => 'font/woff2', 'ttf' => 'font/ttf', 'otf' => 'font/otf', 'eot' => 'application/vnd.ms-fontobject', ]; return $mimeTypes[$extension] ?? 'application/octet-stream'; } ``` ### 보안 규칙 | 규칙 | 설명 | |------|------| | ✅ 화이트리스트에 명시된 확장자만 허용 | 목록 외 차단 | | ✅ 대소문자 구분 없이 검증 | `strtolower()` 사용 | | ✅ 소스맵은 `local` 에서만 허용 | 운영에서 서빙 시 원본 코드 전문 노출 | | 실행 파일 확장자 금지 | `.php`, `.sh`, `.exe` 등 | | ❌ 서버 설정 파일 금지 | `.htaccess`, `.env` 등 | | ❌ 화이트리스트 외 모든 확장자 차단 | 기본 거부 정책 | ### 금지 확장자 예시 (절대 추가 금지) | 카테고리 | 확장자 | |----------|--------| | PHP 실행 파일 | `.php`, `.phar` | | 실행 스크립트 | `.sh`, `.bat`, `.exe` | | 설정 파일 | `.env`, `.htaccess`, `.conf` | | 데이터베이스 파일 | `.sql`, `.db` | | 압축 파일 (보안 위험) | `.zip`, `.tar`, `.gz` | --- ## 관련 문서 - [template-basics.md](template-basics.md) - 템플릿 기초 - [template-routing.md](template-routing.md) - 템플릿 라우트/언어 파일 규칙 - [template-caching.md](template-caching.md) - 템플릿 캐싱 전략 - [template-commands.md](template-commands.md) - 템플릿 Artisan 커맨드