qnaService->getEnquiryCodes(); return view('web.cs.qna.index', [ 'enquiryCodes' => $codes, // select 옵션 ]); } // 등록 public function store(Request $request) { // legacy.auth가 세션에 _sess 넣어준다 했으니 그대로 사용 $sess = (array) $request->session()->get('_sess', []); $memNo = (int)($sess['_mno'] ?? 0); if ($memNo <= 0) { abort(403); } /** * 0) 레이트리밋(봇/대량 등록 방어) * - 회원번호 + IP 기준으로 1분당 5회, 10분당 20회 같은 식으로 강하게 제한 */ $ip = (string) $request->ip(); $key = 'qna:create:' . $memNo . ':' . $ip; if (RateLimiter::tooManyAttempts($key, 5)) { abort(429, '요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.'); } RateLimiter::hit($key, 60); // 60초 $rules = [ 'enquiry_code' => ['required', 'string', 'max:20'], 'enquiry_title' => ['required', 'string', 'max:60'], 'enquiry_content' => ['required', 'string', 'max:5000'], 'return_type' => ['nullable', Rule::in(['sms', 'email'])], 'hp' => ['nullable', 'size:0'], //봇공격 대비 ]; if (app()->environment('production')) { $rules['g-recaptcha-response'] = ['required', new RecaptchaV3Rule('cs_qna_create')]; } $validated = $request->validate($rules); /** * 2) 정규화/무해화 (XSS/스크립트/이상 문자 방어) * - strip_tags는 기본, 추가로 제어문자 제거 + 공백정리 * - HTML은 아예 저장하지 않는 정책이면 가장 안전 */ $validated['enquiry_title'] = $this->sanitizePlainText( (string) $validated['enquiry_title'], 60 ); $validated['enquiry_content'] = $this->sanitizePlainText( (string) $validated['enquiry_content'], 5000 ); /** * 3) “의심 입력” 즉시 차단(선택) * - 운영 경험상 스크립트가 박힌 적이 있다 했으니, * script 태그/iframe/onerror/javascript: 같은 패턴은 바로 차단하는게 낫다. */ $badField = null; if ($this->looksMalicious($validated['enquiry_title'])) $badField = 'enquiry_title'; if ($this->looksMalicious($validated['enquiry_content'])) $badField = $badField ?: 'enquiry_content'; if ($badField) { return redirect()->route('web.cs.qna.index') ->with('ui_dialog', [ 'type' => 'alert', 'title' => '안내', 'message' => '특수 스크립트/태그가 포함되어 등록할 수 없습니다. HTML/스크립트 없이 내용만 작성해 주세요.', ]); } $sess = (array) $request->session()->get('_sess', []); $validated['_user_email'] = (string)($sess['_mid'] ?? ''); $validated['_user_name'] = (string)($sess['_mname'] ?? ''); $validated['_user_cell_enc'] = (string)($sess['_mcell'] ?? ''); $validated['_ip'] = (string)($sess['_ip'] ?? $request->ip()); // 등록 (현재 연도 테이블에 저장) $this->qnaService->createQna($memNo, $validated); // 등록 후 내 문의내역으로 이동 return redirect()->route('web.mypage.qna.index') ->with('ui_dialog', [ 'type' => 'alert', 'title' => '안내', 'message' => '문의가 등록되었습니다.', ]); } private function sanitizePlainText(string $s, int $maxLen): string { // 1) 유니코드 정규화(가능하면) - 없으면 패스 if (class_exists(\Normalizer::class)) { $s = \Normalizer::normalize($s, \Normalizer::FORM_C) ?: $s; } // 2) 태그 제거(HTML 저장 자체를 금지하는 정책) $s = strip_tags($s); // 3) 제어문자 제거(줄바꿈/탭은 허용) // \p{C}는 Control chars 포함. \r\n\t는 남김. $s = preg_replace('/[\p{C}&&[^\r\n\t]]/u', '', $s) ?? $s; // 4) 공백 정리(연속 스페이스/줄바꿈 폭발 방지) $s = preg_replace("/[ \t]+/u", ' ', $s) ?? $s; $s = preg_replace("/\n{4,}/u", "\n\n\n", $s) ?? $s; $s = trim($s); // 5) 길이 자르기(멀티바이트 안전) return Str::limit($s, $maxLen, ''); } private function looksMalicious(string $s): bool { $lower = Str::lower($s); // 너무 공격적으로 잡으면 정상 텍스트도 막을 수 있으니, “확실한 것만” $patterns = [ '