사용자 입력이 그대로 이미지가 될 때 — Next.js ImageResponse RCE로 다시 보는 이스케이프의 기본
next/og의 ImageResponse에서 CVSS 9.5짜리 원격 코드 실행(CVE-2026-94545)이 터졌다. 원인은 이스케이프 하나. 퍼블리셔가 XSS와 싸우며 기른 감각으로, 이 사고를 다시 읽는다.
글을 공유하면 뜨는 미리보기 카드, 그 이미지를 코드로 그려주는 게 요즘 흔해요.
Next.js의 next/og에 있는 ImageResponse가 대표적이죠 — JSX를 넘기면 서버가 그걸 이미지로 렌더해줘요.
그런데 여기서 CVSS 9.5짜리 원격 코드 실행(CVE-2026-94545)이 터졌어요.
”이미지를 만드는 코드”가 서버를 통째로 내줄 수 있는 구멍이 된 거예요.
무슨 일이 있었나
ImageResponse는 내부에서 Satori라는 라이브러리로 JSX를 SVG로 바꾼 다음, 그 SVG를 이미지로 굽습니다.
문제는 그 중간, Satori가 값을 SVG로 옮길 때 제대로 이스케이프하지 않은 것이었어요.
공격자가 넣은 값이 SVG 요소·XML 이름·내부 스타일 필드에서 그냥 텍스트가 아니라 살아 있는 SVG 마크업으로 해석됐거든요.
그러면 이런 일이 벌어져요.
// 사용자가 보낸 이름을 그대로 이미지에 얹는다고 해보면
new ImageResponse(<div>{userName}</div>)
// userName이 평범한 문자열이면 글자로 그려지지만,
// SVG로 해석될 수 있는 값이 이스케이프 없이 들어가면
// 그 값이 '그림'이 아니라 '명령'이 된다.
div 안의 글자가 화면에 찍히는 대신, 그 자리에 끼워 넣은 마크업이 실행 경로로 흘러 들어가요.
결국 이미지 한 장을 만들려던 요청이, 서버에서 코드를 실행시키는 통로가 되는 거예요.
영향 범위는 이래요.
next 16.2.0 이상 16.3.6 미만이 취약하고, 16.3.6에서 패치됐어요(Satori를 직접 쓴다면 0.33.5).
Node.js 런타임의 ImageResponse가 대상이고, Edge 런타임과 15.x는 이 RCE에 해당하지 않아요.
Vercel이 9월 22일 긴급 업데이트로 내보냈고요.
쓰고 있다면, 읽던 걸 멈추고 지금 올리는 게 맞아요.
이건 새 위험이 아니라 옛날 위험이다
여기서 퍼블리셔의 감각이 켜져요.
10년 동안 제가 한 일의 상당 부분은 “사용자가 넣은 글자가 코드가 되지 않게” 막는 거였어요.
게시판에 <script>를 적어도 그게 실행되지 않고 글자로 보이게 하는 것 — 그게 XSS를 막는 이스케이프였죠.
사용자 입력은 데이터지 마크업이 아니다, 이 한 줄이 웹 보안의 오래된 기본이에요.
이번 사고는 그 기본이 “설마 여기서?” 싶은 곳에서 깨진 거예요.
아무도 이미지 만드는 코드를 공격 표면으로 안 봤거든요.
그런데 그 안에서 JSX가 SVG가 되고, SVG는 결국 마크업이라, 이스케이프를 빼먹은 순간 옛날 그 구멍이 그대로 열린 거예요.
이스케이프는 ‘한 번’이 아니라 ‘맥락마다’
제가 이 사고에서 가장 곱씹은 지점이에요.
HTML에 안전하게 이스케이프한 값이, SVG 안에선 다시 위험할 수 있어요.
<, &, 따옴표 같은 글자는 들어가는 언어가 바뀌면 의미도 바뀌거든요.
HTML 맥락, 속성 맥락, URL 맥락, 그리고 이번의 SVG/XML 맥락 — 각각 이스케이프 규칙이 달라요.
그러니 “한 번 처리했으니 안전하다”가 아니라, “이 값이 지금 어느 언어로 들어가고 있나” 를 물어야 해요.
이번 건 그 질문을 라이브러리가 대신 해주는 줄 알았는데, 한 맥락(SVG)에서 놓친 사례였고요.
그럼 우리 블로그는?
이 블로그도 글마다 OG 이미지를 자동으로 만들어요.
그래서 남 얘기로 넘기지 않고 우리 걸 열어봤는데, 다행히 세 겹으로 비켜나 있었어요.
- Next.js가 아니에요. 이 블로그는 Astro라,
next/og자체를 안 써요. - Satori 경로가 아니에요. OG 생성에
astro-og-canvas를 쓰는데, 이건 SVG가 아니라 캔버스(Skia)로 글자를 그려요. SVG 마크업으로 오염될 표면이 없어요. - 입력이 사용자 게 아니에요. 이미지에 들어가는 글자는 내 프론트매터의 제목·설명이고, 빌드 타임에 한 번 구워져요. 런타임에 낯선 사람의 문자열이 흘러들 자리가 없어요.
세 번째가 핵심이에요.
지금 안전한 진짜 이유는 라이브러리가 튼튼해서라기보다, 믿을 수 없는 입력이 그 경로에 애초에 안 닿기 때문이거든요.
(댓글 알림 메일을 plain text로만 보내 HTML 주입을 원천 차단해둔 것과 같은 결의 방어예요.)
그래서 교훈은 “우린 안전하다”가 아니라 이거예요 — 언젠가 OG 이미지에 사용자 입력(댓글 미리보기 같은)을 넣고 싶어지면, 바로 그 순간 이 사고를 떠올리자.
프레임워크는 바뀌어도, 위험의 모양은 그대로
서버가 이미지를 그려주는 건 꽤 새로운 편의예요.
그런데 거기서 터진 위험의 모양은 조금도 새롭지 않았어요 — 사용자 입력이 코드가 되는 순간, 딱 그거였죠.
퍼블리셔로 게시판의 <script>와 싸우던 감각이, 서버 사이드 이미지 생성이라는 낯선 무대에서 그대로 쓰여요.
도구가 바뀔 때 “이건 처음 보는 거니까 조심”보다, “이거 결국 그 옛날 그 문제 아닌가”를 묻는 눈이 더 오래가요.
새 기술의 표면 아래엔, 대개 오래된 기본이 그대로 흐르고 있거든요.
댓글