From 9da494b0e3bbe7b513521d0915510fe9ee376b8b Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 21 Jul 2025 07:19:52 +0000 Subject: (대표님, 최겸) 작업사항 - 이메일 템플릿, 메일링, 기술영업 요구사항 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/mail/mail-template-editor-client.tsx | 205 ++++++++++++++++++++---- 1 file changed, 173 insertions(+), 32 deletions(-) (limited to 'components/mail') diff --git a/components/mail/mail-template-editor-client.tsx b/components/mail/mail-template-editor-client.tsx index dfbeb4e0..c9770aec 100644 --- a/components/mail/mail-template-editor-client.tsx +++ b/components/mail/mail-template-editor-client.tsx @@ -5,7 +5,7 @@ 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 } from 'lucide-react'; +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'; @@ -17,13 +17,81 @@ interface MailTemplateEditorClientProps { initialTemplate?: Template | null; } -export default function MailTemplateEditorClient({ - templateName, - initialTemplate +// 보안: 허용된 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