본문으로 건너뛰기
Security#nginx#CVE#셀프호스팅#보안#취약점대응#Next.js#pnpm audit

Next.js 보안 공지, 우리 스택에 하나씩 대입해 보기

Next.js 10월 보안 공지 3건은 우리 사이트에 해당하지 않았지만, 점검 중 1년 전 DoS 패치를 놓친 것을 찾았습니다. 14.2.35로 올리고 남은 권고 25건을 조건별로 대조해, 해당되는 1건을 nginx로 막은 과정입니다.

Ian4 min read1

Next.js가 10월 14일 긴급 보안 업데이트를 냈습니다. Critical 2건, High 1건이고, 패치는 16.4.1과 15.5.28에만 나왔습니다. 우리 회사 사이트는 Next.js 14에서 돌아갑니다. 14는 공식 지원 정책에서 이미 지원이 끝난 버전입니다. 공지를 그대로 따를 수 없어서, 권고마다 적힌 조건을 우리 스택에 하나씩 대입해 봤습니다.

공지 세 건의 조건

권고문의 Impact 절에는 어떤 앱이 영향을 받는지 적혀 있습니다. 세 건 모두 조건이 분명했습니다.

10월 14일 Next.js 권고 세 건의 발생 조건과 우리 스택을 나란히 놓고 모두 해당 없음으로 표시한 표

  • use cache 원격 코드 실행: Cache Components를 쓰는 App Router 앱이 대상입니다. Next 14에는 이 기능이 없습니다.
  • 이미지 최적화 SVG 원격 코드 실행: glibc 기반 리눅스에서 이미지 라이브러리의 메모리 버그로 생깁니다. 우리 컨테이너는 musl을 쓰는 Alpine이고, web 컨테이너에서는 Next가 불러올 sharp가 없습니다. Next 14는 sharp가 없으면 내장 squoosh로 이미지를 처리합니다.
  • Server Actions 서비스 거부: Server Action이 하나라도 있는 앱이 대상입니다. web과 admin 어디에도 "use server"가 없습니다.

점검에 쓴 명령은 다음과 같습니다.

bash
# 코드: 조건이 되는 기능을 쓰는지
grep -rln -e '"use cache"' -e '"use server"' apps/*/src
# 운영 컨테이너: 실제 Next 버전과 OS
docker compose exec web node -p "require('next/package.json').version"
docker compose exec web cat /etc/os-release

lockfile의 경고

조건만 보면 여기서 끝날 일이었습니다. 그런데 버전을 확인하다가 pnpm-lock.yaml에서 경고를 봤습니다. 우리가 쓰던 14.2.15에 "This version has a security vulnerability"라는 deprecated 표시가 붙어 있었고, 링크는 10월 공지가 아니라 2025년 12월 공지였습니다.

그 공지의 CVE-2025-55184(High)는 조작한 요청 하나로 App Router 서버를 무한 루프에 빠뜨리는 취약점입니다. 첫 수정이 불완전해서 CVE-2025-67779로 다시 고쳐졌고, 14.x는 14.2.35에서 해결됐습니다. 우회 방법은 없습니다.

Server Action이 없는 우리 앱도 해당된 이유는 두 버전의 코드를 비교하고서야 알았습니다. 14.2.15는 일부 요청 형식에서 Action ID를 확인하기 전에 본문부터 해석했습니다. 14.2.35에는 이 검사가 들어갔습니다.

javascript
// next 14.2.35: dist/server/app-render/action-handler.js
// If the app has no server actions at all, we can 404 early.
if (!hasServerActions(serverActionsManifest)) {
    const error = getActionNotFoundError(actionId);
    console.warn(error);
    return {
        type: "not-found"
    };
}

14.2.15는 Server Action 요청의 본문을 먼저 해석하고, 14.2.35는 앱에 Server Action이 없으면 해석 전에 404로 끝내는 흐름 비교

web과 admin을 14.2.35로 올렸습니다. 빌드와 타입 검사를 통과했고, 지난번 ISR 캐시 시나리오도 이전과 같은 결과를 냈습니다.

남은 25건 분류

pnpm audit으로 보면 next 권고는 34건에서 25건으로 줄었습니다. 남은 25건은 모두 15.5 이상에서만 고쳐졌고, 14.x에는 앞으로도 패치가 나오지 않습니다.

pnpm audit의 next 권고가 14.2.15의 34건에서 14.2.35의 25건으로 줄어든 심각도별 막대 차트

25건을 발생 조건으로 나눴습니다.

남은 25건을 Server Actions 8건, 미들웨어·rewrites 4건, 공유 캐시 3건, 특정 API 3건, 호스팅 환경 2건, sharp 2건, 이미지 최적화 3건으로 나누고 디스크 캐시 1건만 막았다고 표시한 그림

대부분은 우리가 쓰지 않는 기능이 조건이었습니다. 환경 조건도 확인했습니다. WebSocket 업그레이드를 통한 SSRF는 엣지와 홈서버의 nginx가 Upgrade 헤더를 넘기지 않아서 Next까지 오지 않습니다. RSC 캐시 오염은 공유 캐시가 있어야 생기는데, 우리는 CDN도 nginx 캐시도 쓰지 않습니다.

기본 이미지 로더로 셀프호스팅하면 해당되는 이미지 최적화 3건이 남았습니다.

  • 원격 이미지 메모리 문제: 허용한 원격 주소가 우리 API의 업로드 경로뿐이라, 공격자가 그곳에 큰 이미지를 둘 수 없습니다.
  • 로컬 이미지 메모리 문제: 가장 큰 로컬 이미지가 826KB라 상한이 작습니다.
  • 디스크 캐시 무한 증가: 해당됩니다. 품질 값 q를 1부터 100까지 바꿔 요청하면 모두 다른 파일로 캐시됩니다.

nginx로 막은 것

운영 로그에 남은 /_next/image 요청 1007건은 모두 q=75였습니다. next/image의 기본값입니다. 너비 w는 Next가 정해진 16개 값만 받습니다. 그래서 처음에는 홈서버 nginx에서 다른 품질 값만 막았습니다.

로컬에서 더 시험해 보니 품질 값만의 문제가 아니었습니다. 이미지 주소 뒤에 쿼리나 #, 공백을 붙이거나 :443을 적어도 같은 이미지가 새 파일로 캐시됐습니다. 또 nginx의 $arg_q는 파라미터 이름의 대소문자를 가리지 않아서, Q=75&q=50을 보내면 nginx는 75를, Next는 50을 읽었습니다. 그래서 규칙을 세 줄로 늘렸습니다.

nginx
# 사이트 server 블록, location / 위에
location = /_next/image {
    # 같은 파라미터가 두 번 오면 거절 (이름은 대소문자 무시)
    if ($args ~* "(?:^|&)(q|url|w)=.*&\1=") { return 400; }
    if ($arg_q != "75") { return 400; }
    # 사이트가 실제로 쓰는 두 형태만: 로컬 /images와 API 업로드
    if ($arg_url !~ "^(%2Fimages(%2F[A-Za-z0-9._-]+)+|https(%3A%2F%2F|://)api\.fleanlabs\.io(%2F|/)uploads(%2F|/)[0-9a-f]{32}\.webp)$") { return 400; }
    # 나머지 proxy 설정은 location / 와 같게
}

적용 뒤 정상 요청은 200, 위의 변형을 포함한 11가지 요청은 모두 400을 냅니다. 사이트 페이지 13개가 쓰는 이미지 주소 42개는 모두 그대로 열립니다. 이제 캐시 파일 수는 이미지 수 × 너비 16개 × 포맷 2개를 넘지 않습니다.

앱 설정으로 막는 방법도 있습니다. 14.2.35에는 14.2.15에 없던 images.qualities 옵션이 있어서, qualities: [75]를 두면 Next가 다른 품질 값을 400으로 돌려줍니다. 로컬에서 확인했습니다. 다만 품질 값만 막으므로 주소 변형은 따로 막아야 합니다.

마치며

  • 보안 공지는 패치 버전보다 영향 조건을 먼저 읽습니다. 10월 공지 세 건은 우리에게 해당하지 않았습니다.
  • 공지만 보면 놓치는 것이 있습니다. lockfile의 deprecated 표시와 pnpm audit이 1년 전 패치를 놓친 것을 알려 줬습니다.
  • 지원이 끝난 버전에서는 남은 권고를 조건별로 대조하고, 해당하는 것은 앞단에서 막습니다. 막은 뒤에는 우회 경로까지 직접 시험합니다.
  • 이것은 임시 조치입니다. 14.x에는 더 이상 패치가 나오지 않으니 16으로 옮길 계획입니다.

지원이 끝난 버전에 머물면 공지가 나올 때마다 이런 대조를 직접 해야 합니다. 그 비용이 업그레이드 비용보다 커지기 전에 옮기려 합니다.

플린랩스와 함께

함께 미래를 만들 준비가 되셨나요?

기획·설계부터 구축, 운영까지 — 플린랩스가 전 과정을 함께합니다.

인천, 북위 37.53도 동경 126.73도