'use client'; import { useState, useEffect, useTransition } from 'react'; import { useRouter, useParams } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Save, Eye, AlertTriangle } from 'lucide-react'; import { toast } from 'sonner'; import Link from 'next/link'; import { getTemplateAction, updateTemplateAction, previewTemplateAction, TemplateFile } from '@/lib/mail/service'; type Template = TemplateFile; interface MailTemplateEditorClientProps { templateName: string; initialTemplate?: Template | null; } // 보안: 허용된 Handlebars 헬퍼와 변수만 정의 const ALLOWED_VARIABLES = [ 'userName', 'companyName', 'email', 'date', 'projectName', 'message', 'currentYear', 'language', 'name', 'loginUrl' ]; const ALLOWED_HELPERS = ['if', 'unless', 'each', 'with']; // 보안: 위험한 패턴 탐지 const DANGEROUS_PATTERNS = [ /\{\{\s*(constructor|prototype|__proto__|process|global|require|import|eval|Function)\s*\}\}/gi, /\{\{\s*.*\.(constructor|prototype|__proto__)\s*.*\}\}/gi, /\{\{\s*.*\[\s*['"`]constructor['"`]\s*\]\s*.*\}\}/gi, /\{\{\s*.*require\s*\(.*\)\s*.*\}\}/gi, /\{\{\s*.*process\s*\..*\}\}/gi, /\{\{\s*.*global\s*\..*\}\}/gi, /\{\{\s*.*this\s*\..*\}\}/gi, /\{\{\s*#with\s+.*\.\.\s*\}\}/gi, // path traversal ]; // 보안: 템플릿 내용 검증 const validateTemplateContent = (content: string): { isValid: boolean; errors: string[] } => { const errors: string[] = []; // 위험한 패턴 검사 for (const pattern of DANGEROUS_PATTERNS) { if (pattern.test(content)) { errors.push('보안상 위험한 구문이 감지되었습니다.'); break; } } // 허용되지 않은 변수 검사 const variableMatches = content.match(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g); if (variableMatches) { for (const match of variableMatches) { const variable = match.replace(/\{\{\s*|\s*\}\}/g, ''); if (!ALLOWED_VARIABLES.includes(variable)) { errors.push(`허용되지 않은 변수 '${variable}'가 사용되었습니다.`); } } } // 허용되지 않은 헬퍼 검사 const helperMatches = content.match(/\{\{\s*#([a-zA-Z_][a-zA-Z0-9_]*)/g); if (helperMatches) { for (const match of helperMatches) { const helper = match.replace(/\{\{\s*#/, ''); if (!ALLOWED_HELPERS.includes(helper)) { errors.push(`허용되지 않은 헬퍼 '${helper}'가 사용되었습니다.`); } } } return { isValid: errors.length === 0, errors }; }; // 보안: HTML 출력 무력화 ({{{html}}} 형태 방지) const sanitizeTripleBraces = (content: string): string => { return content.replace(/\{\{\{([^}]+)\}\}\}/g, (match, variable) => { // HTML 출력을 일반 변수 출력으로 변환 return `{{${variable.trim()}}}`; }); }; export default function MailTemplateEditorClient({ templateName, initialTemplate }: MailTemplateEditorClientProps) { const router = useRouter(); const params = useParams(); const lng = (params?.lng as string) || 'ko'; const [template, setTemplate] = useState