Files
Gnuboard7/app/Http/Middleware/SetLocale.php
T
HeuJung cf1d8ab52c fix(security): 보안 제보 14건 + 유사 결함 전수조사 수정
sir.kr 커뮤니티 제보(Xbuilder 님 외)와 전수조사 발굴분을 코어·게시판·
이커머스·페이지·ckeditor5 전반에 반영.

- 통합 검색 하이라이트: 코어 SearchHighlighter 로 원문 이스케이프 후 강조
 (저장형 XSS 차단, board/ecommerce/page 3중복제 통합)
- 공개 최신글·인기글·관리자 대시보드: 읽기권한 없는 게시판·블라인드·비활성
 게시판 글 필터. 비밀글은 기존 공개 정책대로 제목 노출 유지(본문만 보호)
- 게시판 관리 대시보드 권한 게이트, 이커머스 쿠폰 다운로드·리뷰 대상·주문옵션
 확정 스코프 강화, 취소/환불 항목 검증 회원/비회원 대칭
- 만료 토큰 거부(SetLocale), 회원 탈퇴·정보관리 실패 시 내부 오류 원문 마스킹
- 예외→응답 매핑 정비(generic catch 상태코드), ckeditor 업로드 권한 게이트 정리

정책 정합 정정: 착수 계획이 "비밀글 제목 노출=취약점"으로 전제해 공개 최신글·
대시보드에 넣었던 is_secret 필터는, 저장소가 확정한 "제목 공개, 본문만 보호"
정책(2026-01-02/02-04/06-18/06-19)과 배치되어 되돌림. 읽기권한·블라인드 필터는 유지.
Chrome MCP 실측(T1~T10) + 회귀 테스트로 확인.
2026-08-17 15:46:38 +09:00

133 lines
4.2 KiB
PHP

<?php
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Laravel\Sanctum\PersonalAccessToken;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
/**
* 들어오는 요청을 처리하고 사용자 언어를 설정합니다.
*
* @param Request $request HTTP 요청
* @param Closure $next 다음 미들웨어 또는 요청 핸들러
* @return Response HTTP 응답
*/
public function handle(Request $request, Closure $next): Response
{
$locale = $this->determineLocale($request);
App::setLocale($locale);
return $next($request);
}
/**
* 사용자의 언어 설정을 결정합니다.
*
* 우선순위:
* 1. 로그인한 사용자의 언어 설정 (users.language)
* 2. 브라우저 Accept-Language 헤더
* 3. 시스템 기본값 (config('app.locale'))
*
* 참고: 프론트엔드(localStorage g7_locale)에서 언어를 관리하므로
* 서버는 사용자 DB 설정만 우선 적용합니다.
*
* @param Request $request HTTP 요청
* @return string 결정된 언어 코드
*/
private function determineLocale(Request $request): string
{
$supportedLocales = config('app.supported_locales', ['ko', 'en']);
// 1. 인증된 사용자의 언어 설정 확인 (최우선)
$user = $this->resolveUser($request);
if ($user && $user->language) {
$userLocale = $user->language;
if (in_array($userLocale, $supportedLocales)) {
return $userLocale;
}
}
// 2. Accept-Language 헤더 확인 (브라우저 기본 언어)
$acceptLanguage = $request->header('Accept-Language');
if ($acceptLanguage) {
$locale = $this->parseAcceptLanguage($acceptLanguage);
if (in_array($locale, $supportedLocales)) {
return $locale;
}
}
// 3. 기본값 반환
return config('app.locale', 'ko');
}
/**
* 요청에서 사용자를 가져옵니다.
*
* 이 미들웨어는 auth:sanctum보다 먼저 실행되므로
* Auth::check()가 작동하지 않을 수 있습니다.
* 따라서 Bearer 토큰을 직접 파싱하여 사용자를 가져옵니다.
*
* @param Request $request HTTP 요청
* @return User|null 사용자 또는 null
*/
private function resolveUser(Request $request): ?User
{
// 이미 인증된 경우 (세션 기반 인증)
if (Auth::check()) {
return Auth::user();
}
// Bearer 토큰이 있는 경우 (API 토큰 인증)
$bearerToken = $request->bearerToken();
if ($bearerToken) {
$token = PersonalAccessToken::findToken($bearerToken);
// 만료된 토큰은 인증되지 않은 것으로 취급 (guest 로케일 폴백).
// OptionalSanctumMiddleware 와 동일한 만료 검사를 적용한다.
if ($token && $token->expires_at && $token->expires_at->isPast()) {
return null;
}
if ($token && $token->tokenable instanceof User) {
return $token->tokenable;
}
}
return null;
}
/**
* Accept-Language 헤더를 파싱하여 선호 언어를 추출합니다.
*
* @param string $acceptLanguage Accept-Language 헤더 값
* @return string 추출된 언어 코드 (기본값: config('app.locale'))
*/
private function parseAcceptLanguage(string $acceptLanguage): string
{
$supportedLocales = config('app.supported_locales', ['ko', 'en']);
$languages = explode(',', $acceptLanguage);
foreach ($languages as $language) {
$lang = trim(explode(';', $language)[0]);
// 언어-지역 형태에서 언어만 추출 (예: ko-KR -> ko)
if (strpos($lang, '-') !== false) {
$lang = explode('-', $lang)[0];
}
if (in_array($lang, $supportedLocales)) {
return $lang;
}
}
return config('app.locale', 'ko');
}
}