Next.js 9월 보안 업데이트 정리 — next/og RCE부터 SSRF까지, 16.3.8·15.5.27로 올려야 하는 이유
Next.js 9월 보안 업데이트는 두 번에 나눠 나왔다. 9월 22일에는 next/og의 Node.js ImageResponse에서 원격 코드 실행(RCE)으로 이어질 수 있는 치명적(Critical) 문제를 고친 16.3.6·15.5.26이 긴급 배포됐고, 9월 30일에는 이미지 최적화 SSRF를 포함한 취약점 7건(High 1, Medium 5, Low 1)을 고친 **16.3.8(Active LTS)·15.5.27(Maintenance LTS)**이 정기 보안 릴리스로 나왔다. 결론은 간단하다. 16.x를 쓰면 16.3.8, 15.5를 쓰면 15.5.27로 올리면 두 번의 수정이 모두 들어간다.
다만 이번 릴리스엔 덧붙일 사정이 하나 있다. 9월 23일 사전 공지에서 Next.js 팀은 9건을 고치겠다고 했는데, 실제 30일 릴리스에서는 7건만 다뤘다. 남은 2건(Critical 1, High 1)은 업스트림 의존성 쪽 조율이 늦어져 다음 릴리스로 미뤄졌다. 10월 5일 기준 npm의 latest 태그는 여전히 16.3.8이다. 아직 끝난 이야기가 아니라는 뜻이다.
9월 22일 긴급 업데이트 — next/og RCE
먼저 터진 건 이미지 생성 쪽이다. next/og의 ImageResponse는 JSX로 OG 이미지(링크 미리보기 썸네일)를 만들어 주는 API인데, 내부에서 Vercel의 오픈소스 라이브러리 Satori가 JSX를 SVG로 바꾸고 그 SVG를 PNG로 굽는다. 이 Satori가 일부 값을 제대로 이스케이프하지 않아, 조작된 입력이 SVG 마크업으로 해석될 수 있었다(Satori 권고문 GHSA-wx4j-mvgx-mqwp, 0.33.5에서 수정). 그 자체는 Medium 등급이다. 문제는 이렇게 만들어진 SVG를 받아 처리하는 다른 업스트림 의존성의 취약점과 엮이면서 Next.js에서는 원격 코드 실행까지 가능해졌다는 점이고, 그래서 Next.js 쪽 권고문(GHSA-vcvr-r3jv-pc5j)은 Critical로 매겨졌다.
조건은 꽤 구체적이다.
- 영향 버전: Next.js
>=16.2.0 <16.3.6. 15.x는 RCE 대상이 아니고, 15.5.26은 관련 보강(hardening)만 담았다. - 영향받는 코드: Node.js 런타임의
ImageResponse에 공격자가 조작할 수 있는 값(쿼리스트링 등)을 SVG 내용·속성·스타일로 넘기는 경우. 권고문 예시는searchParams.get('value')를 그대로<svg>안<title>에 넣는 라우트 핸들러다. - 영향 없음: Edge 런타임의
ImageResponse를 쓰거나, 사용자 입력을 SVG에 넣지 않는 경우.
당장 올릴 수 없다면 권고문이 제시하는 우회책은 하나다. Node.js ImageResponse가 그리는 SVG 내용·속성·스타일에 공격자가 조작할 수 있는 값을 넣지 않는 것. 고정된 아이콘이나 빌드 시점 데이터만 그리는 icon.tsx 같은 파일이라면 실질 위험은 낮지만, 블로그 글 제목을 URL로 받아 OG 이미지를 동적으로 만드는 흔한 패턴은 정확히 이 조건에 걸릴 수 있다.
9월 30일 정기 릴리스 — 7건 한눈에 보기
두 번째 릴리스는 성격이 다르다. 한 건의 치명적 문제가 아니라, 기능별로 조건이 붙은 여러 건이 묶여 있다. GitHub의 vercel/next.js 저장소 보안 권고를 gh api로 직접 조회해(2026-10-05) 등급·CVSS 점수를 맞춰 봤다.
| 취약점 | 등급(CVSS) | 영향받는 조건 |
|---|---|---|
| 이미지 최적화 SSRF (CVE-2026-94483) | High (8.3) | images.remotePatterns에 허용한 원격 호스트가 공격자 손에 들어간 경우. 설정이 없으면 무관 |
| Pages Router SSG/ISR 캐시 오염 (CVE-2026-94543) | Medium (6.3) | 자체 호스팅 + Pages Router SSG/ISR. Vercel 배포는 영향 없음 |
| 루트 catch-all + SSG/ISR 캐시 오염 (CVE-2026-94484) | Medium (6.3) | 루트 레벨 catch-all 페이지와 SSG/ISR 라우트를 함께 쓰는 앱 |
메타데이터 이미지 dynamicParams 우회 (CVE-2026-94485) | Medium (6.3) | App Router + webpack 빌드. Turbopack 빌드는 영향 없음 |
중첩 'use cache' 루트 파라미터 캐시 누출 (GHSA-h694-7cp9-m8p3) | Medium (6.3) | Cache Components 사용 시. 우회책 없음 |
use cache 채우기 중 Draft Mode 내용 누출 (CVE-2026-94544) | Medium (6.3) | Cache Components + Draft Mode 미리보기 |
| 개발 서버 MCP 엔드포인트 정보 노출 (CVE-2026-94486) | Low (2.3) | next dev로 실행할 때만. 운영 배포는 무관 |
몇 개는 짚고 넘어갈 만하다. SSRF(서버 측 요청 위조)는 공격자가 서버를 시켜 원래 닿으면 안 되는 내부 주소(사설 IP 대역 등)로 요청을 보내게 만드는 공격이다. 권고문의 우회책을 보면 핵심은 DNS다. remotePatterns에 넣은 호스트 중 DNS 레코드를 믿기 어려운 곳이 있는지 점검하라고 한다. 허용 목록에 있는 도메인이라도 그 도메인의 DNS가 내부 IP를 가리키게 바뀌면 이미지 최적화 서버가 내부망으로 요청을 날리는 구조라는 얘기다.
Draft Mode 건은 헤드리스 CMS를 붙인 사이트라면 꼭 봐야 한다. 편집자가 미리보기를 여는 순간 같은 캐시 키로 들어온 일반 방문자의 요청이 겹치면, 인증 없이 미공개 원고를 받아 간다. 그 요청이 페이지를 사전 렌더링하는 중이었다면 미공개 내용이 생성된 페이지에 박혀 재검증 전까지 모든 방문자에게 나간다. 사고가 나도 "누가 해킹했다"가 아니라 "엠바고 기사가 먼저 떴다"로 드러날 유형이다.
MCP 엔드포인트 건은 등급은 낮지만 요즘 흐름을 보여 준다. MCP(Model Context Protocol)는 AI 코딩 도구가 개발 서버에서 라우트 목록·오류·로그를 읽어 가도록 만든 표준 연결 방식이다. next dev가 이 엔드포인트를 열어 두면서 요청 출처를 확인하지 않았고, 그래서 개발자가 띄워 둔 브라우저 탭의 악성 사이트가 프로젝트 경로, 오류 리포트 속 소스 일부, 개발 로그를 읽어 갈 수 있었다.
자체 호스팅과 Vercel, 무엇이 다른가
공식 발표에서 "Vercel 배포는 영향 없음"이라고 명시한 건 7건 중 Pages Router SSG/ISR 캐시 오염 한 건뿐이다. 나머지는 Vercel에 대한 별도 언급이 없다. 그러니 "Vercel에 올려 두었으니 괜찮다"는 판단은 근거가 약하다. Next.js는 플랫폼이 아니라 프로젝트의 npm 의존성이고, 배포 결과물에는 package.json과 lockfile이 고정한 버전이 들어간다. Vercel이든 자체 서버든 의존성을 올리고 다시 배포해야 수정이 반영된다.
자체 호스팅 쪽이 따로 챙길 지점도 있다. 캐시 오염 두 건은 Next.js 서버가 공유 응답 캐시를 직접 들고 있는 구조에서 피해가 커진다. 업그레이드 뒤에도 이미 오염된 캐시 항목은 재검증 전까지 남을 수 있으니, ISR 페이지가 이상한 내용을 보여 준 적이 있다면 배포와 함께 캐시를 비우는 편이 깔끔하다. Edge 런타임 ImageResponse, Turbopack 빌드처럼 "이 구성이면 영향 없음"이 명시된 경우도 있지만, 어느 쪽이든 16.3.8로 올리면 따질 필요가 없어진다.
업그레이드 방법과 실제로 확인해 본 것
공식 안내대로라면 명령은 한 줄이다.
npm install [email protected] # 16.x
npm install [email protected] # 15.5
16.0·16.1·16.2를 쓰고 있다면 같은 마이너 안에서 고친 버전은 없다. npm 기록상 16.2 계열 마지막 릴리스는 7월 25일의 16.2.12이고, 9월 수정은 전부 16.3 계열로만 나왔다. 16.3.8로 올라가야 한다. 중간에 9월 29일 나온 16.3.7은 버그 수정 릴리스로, 9월 30일 보안 수정은 들어 있지 않다. 16.3.7에서 멈추면 안 된다.
npm 레지스트리를 직접 조회해 보면 배포 시각도 공지와 맞는다(2026-10-05, Node.js 22.16.0·npm 10.9.2, macOS).
npm view next dist-tags
# latest: '16.3.8', backport: '15.5.27', canary: '16.4.0-canary.60'
npm view next time --json으로 본 게시 시각은 16.3.6이 9월 22일 16:19(UTC), 15.5.26이 같은 날 17:03, 16.3.8이 9월 30일 16:07, 15.5.27이 16:19였다.
여기서 하나 걸리는 게 나왔다. npm audit이 내부적으로 쓰는 npm 공개 취약점 조회 API에 버전을 넣어 봤다.
curl -s -X POST https://registry.npmjs.org/-/npm/v1/security/advisories/bulk \
-H 'content-type: application/json' -d '{"next":["16.3.6"]}'
# {}
16.3.4를 넣으면 9월 22일 RCE 한 건이 나온다. 그런데 16.3.6은 빈 결과다. 9월 30일 7건이 아직 GitHub 전역 권고 데이터베이스(npm audit이 참조하는 곳)에 올라가지 않았기 때문이다. 저장소 권고 자체는 공개돼 있지만, 패치 버전 칸이 아직 16.3.?로 비어 있다. 지금 시점엔 npm audit이 깨끗하다고 해서 9월 30일 수정까지 들어갔다는 보장이 없다. Dependabot이나 audit 기반 CI 게이트만 믿는 팀이라면 이번엔 버전을 직접 확인하는 수밖에 없다.
우리 프로젝트에 적용한다면
개인적으로는 "어느 취약점이 우리 해당인지" 따지는 시간보다 16.3.8로 올리는 시간이 짧다고 본다. 그래도 순서를 정하자면 이렇다.
npm ls next로 실제 설치 버전을 본다. 모노레포라면 패키지마다 다를 수 있다.- 16.2.0~16.3.5이고 Node.js 런타임
ImageResponse에 쿼리스트링·경로 파라미터를 넣는 코드가 있으면 최우선이다.grep -rn "next/og"로 쓰는 곳부터 찾는다. next.config의images.remotePatterns에 사용자 업로드 버킷이나 외부 CDN처럼 DNS를 우리가 통제하지 않는 호스트가 있는지 본다.- 16.3.8 또는 15.5.27로 올리고 lockfile까지 커밋한 뒤 다시 배포한다. 자체 호스팅이라면 ISR 캐시도 같이 비운다.
- 아직 남은 Critical·High 2건 공지를 기다린다. 8월과 9월 정기 릴리스 모두 일주일쯤 앞서 Next.js 블로그에 사전 공지가 먼저 올라왔다.
8월 25일 정기 릴리스(16.3.3·15.5.24)에서도 AVIF 이미지 최적화 RCE와 Windows 서버 RCE, Critical 두 건이 나왔다. 8월부터 따지면 이미지 처리 경로(이미지 최적화, OG 이미지 생성)에서만 Critical 두 건과 High 한 건이 연달아 나온 셈이다. 이미지 관련 기능을 많이 쓰는 서비스일수록 다음 공지가 뜨는 날 바로 올릴 수 있게 의존성 업그레이드 경로를 짧게 유지해 두는 게 실질적인 대비다.