Files
Gnuboard7/docs/extension/template-security.md
T
HeuJung c9a73e7877 chore(core,ecommerce,tosspayments,pay_kginicis,message_bizppurio,admin_basic): 리베이스 후속 정합화 — 보류 테스트 정리·소스맵 게이트 강제
develop 리베이스 완주 후 보류 항목 처리.

- ApiDocPipelineTest 의 예시값 갱신 단언을 병합 계약(타입 동일 시 보존 —
 멱등)에 정합화. 예시값 최신성 단언과 멱등성 설계가 같은 상황에 정반대를
 요구하던 stale test 로, 병합에서 의도 채택한 쪽은 멱등성이다.
- 환경설정 보안 탭 체크박스 2개의 label prop 을 Label 래핑+형제 Span 으로
 교체. label prop 은 렌더되지 않아 설명 문구가 화면에 없었다.
- 확장 vite config 의 sourcemap: true 하드코딩이 --production 의
 G7_BUILD_SOURCEMAP=0 주입을 조용히 무시해 배포본 dist 에 .map 과 dangling
 참조가 남던 문제를 tosspayments·message_bizppurio 에서 정정하고 production
 재빌드. template-security.md 에 게이트 형태를 명시하고 audit 룰
 vite-sourcemap-env-gate(error) 를 신설해 재발을 차단한다.
- PHPUnit 12 대비: 접촉 테스트 파일의 docblock 메타데이터 6건 attribute 전환.
2026-08-06 15:00:23 +09:00

12 KiB

템플릿 보안 정책

목적: 템플릿 시스템의 보안 정책 및 API 서빙 규칙 가이드


목차

  1. 레이아웃 JSON 검증
  2. 의존성 검증
  3. XSS 방지
  4. 템플릿 API 서빙 규칙
  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)

예시

// 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 엔드포인트

// routes/api.php
Route::get('/templates/{identifier}/assets/{file}', [TemplateAssetController::class, 'serve'])
    ->where('file', '.*')
    ->name('api.templates.assets');

Controller 구현 (TemplateAssetController)

<?php

namespace App\Http\Controllers\Api\Public;

use App\Http\Controllers\Api\Base\PublicBaseController;
use App\Services\Template\TemplateService;
use Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

class TemplateAssetController extends PublicBaseController
{
    public function __construct(
        private TemplateService $templateService
    ) {}

    /**
     * 템플릿 에셋 파일 제공
     *
     * @param string $identifier 템플릿 식별자
     * @param string $file 파일 경로 (예: dist/components.js)
     * @return BinaryFileResponse|Response
     */
    public function serve(string $identifier, string $file): BinaryFileResponse|Response
    {
        // 1. 템플릿 활성화 여부 확인
        if (!$this->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

namespace App\Services\Template;

class TemplateService
{
    /**
     * 템플릿 활성화 여부 확인
     */
    public function isTemplateActive(string $identifier): bool
    {
        return Cache::remember(
            "template.active.{$identifier}",
            3600,
            fn() => Template::where('identifier', $identifier)
                ->where('is_active', true)
                ->exists()
        );
    }
}

Blade 사용 예시

<!-- resources/views/app.blade.php -->
<!DOCTYPE html>
<html>
<head>
    <!-- 활성 템플릿 컴포넌트 로드 -->
    @if($activeTemplate)
        <script src="{{ route('api.templates.assets', [
            'identifier' => $activeTemplate->identifier,
            'file' => 'dist/components.js'
        ]) }}" defer></script>
    @endif
</head>
<body>
    <!-- 앱 컨텐츠 -->
</body>
</html>

보안 규칙

규칙 설명
✅ 활성화된 템플릿만 접근 가능 비활성 템플릿 에셋 접근 차단
✅ 화이트리스트 기반 확장자 검증 허용된 확장자만 제공
✅ 디렉토리 탈출 공격 방지 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 의 소스맵 선언은 다음 형태만 사용한다:

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 구현

// 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

관련 문서