테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

원격 코드 실행 사례

Log4Shell부터 Next.js까지, 바깥에서 보낸 데이터가 서버에서 코드로 실행된 사건들과 그 공통 원리, 대응 순서다.

목차 (9)
  1. 데이터가 코드가 되는 지점
  2. Log4Shell (2021, Log4j 2)
  3. Spring4Shell (2022, Spring Framework 데이터 바인딩)
  4. Confluence OGNL 인젝션 (2022)
  5. 자바 역직렬화의 뿌리 (2015, Commons Collections)
  6. PHP-CGI 인자 주입 (2024, CVE-2024-4577)
  7. React Server Components·Next.js RCE (2025)
  8. 비교: Next.js 미들웨어 인가 우회 (2025, CVE-2025-29927)
  9. 대응의 공통 틀

데이터가 코드가 되는 지점

원격 코드 실행(RCE, remote code execution)은 바깥에서 보낸 요청 하나로 서버가 공격자가 고른 코드를 실행하게 되는 취약점이다. 서버 프로세스의 권한을 통째로 넘겨주는 셈이라 CVSS 점수가 거의 늘 9점을 넘는다. 아래 사례들은 언어도 프레임워크도 다르지만 원리는 하나다. 프로그램이 데이터로 받은 글자를 어딘가에서 코드처럼 해석했다. 웹 보안 노트의 SQL 인젝션도 같은 뿌리인데, 여기서는 해석기가 데이터베이스가 아니라 언어 런타임 자체라 피해가 더 크다.

갈래데이터가 코드가 되는 곳이 글의 사례
역직렬화바이트를 객체로 되살리며 그 객체의 메서드가 불린다Commons Collections(2015), React Server Components(2025)
표현식 언어입력이 템플릿·식 평가기에 들어가 계산된다Confluence OGNL(2022)
동적 조회·로딩문자열이 가리키는 이름·주소로 객체나 클래스를 찾아 불러온다Log4Shell(2021), Spring4Shell(2022)
인자 주입입력이 다른 프로그램의 명령줄 옵션으로 넘어간다PHP-CGI(2012, 2024)
데이터가 코드가 되는 네 갈래: 바깥 입력이 역직렬화, 표현식 언어, 동적 조회와 로딩, 인자 주입 중 하나를 거쳐 코드로 해석된다. 역직렬화에는 Commons Collections와 React Server Components, 표현식 언어에는 Confluence OGNL, 동적 조회에는 Log4Shell과 Spring4Shell, 인자 주입에는 PHP-CGI 사례가 놓인다갈래이 글의 사례바깥 입력요청·로그·파일역직렬화바이트 → 객체Commons Collections (2015)React RSC (2025)표현식 언어입력 → 식 평가Confluence OGNL (2022)동적 조회·로딩이름 → 객체·클래스Log4Shell (2021)Spring4Shell (2022)인자 주입입력 → 명령줄 옵션PHP-CGI (2012, 2024)
데이터가 코드가 되는 네 갈래와 이 글의 사례. 어느 갈래든 바깥 입력이 신뢰 경계를 넘어 해석기에 닿는 순간이 위험하다. Spring4Shell은 속성 경로를 따라 내부 객체를 찾아가는 바인딩이라 동적 조회 쪽에 두었다.

사례마다 무엇이 일어났나 → 왜 그렇게 됐나 → 영향 범위와 고친 판 → 배울 점 순서로 본다. 공격 문자열이나 재현 절차는 싣지 않는다. 막는 법을 이해하는 데는 원리면 충분하다.

해사례스택CVE갈래고친 판(대표)
2015Commons Collections 가젯Java 직렬화라이브러리 CVE 없음(제품별 CVE-2015-4852 등)역직렬화3.2.2, 4.1
2021Log4ShellJava, Log4j 2CVE-2021-44228동적 조회·로딩2.17.1(후속 수정 포함)
2022Spring4ShellJava, Spring FrameworkCVE-2022-22965동적 조회(바인딩)5.3.18, 5.2.20
2022Confluence OGNLJava, ConfluenceCVE-2022-26134표현식 언어7.18.1 등 판별
2024PHP-CGI Best-FitPHP, WindowsCVE-2024-4577인자 주입8.3.8, 8.2.20, 8.1.29
2025React Server ComponentsJavaScript, React·Next.jsCVE-2025-55182역직렬화React 19.0.1, 19.1.2, 19.2.1
2025(비교) Next.js 미들웨어 우회JavaScript, Next.jsCVE-2025-29927인가 우회(RCE 아님)15.2.3 등 판별

Log4Shell (2021, Log4j 2)

무엇이 일어났나

2021년 12월 10일 자바 로깅 라이브러리 Apache Log4j 2의 취약점 CVE-2021-44228이 공개됐다. CVSS 10.0이고, CISA는 같은 날 이를 알려진 악용 취약점(KEV) 목록에 올렸다. 로그에 남는 문자열에 특정 꼴의 글자(${jndi:…})가 들어 있으면, 서버가 그 글자가 가리키는 바깥 서버에 접속해 클래스를 받아 실행할 수 있었다. 사용자 이름, 검색어, 요청 헤더처럼 "그냥 기록만 하는" 값이 입구가 됐다.

왜 그렇게 됐나

Log4j 2에는 로그 메시지 안의 ${…}를 실제 값(환경 변수, 시스템 속성 등)으로 바꿔 주는 lookup 기능이 있었다. 그중 JNDI lookup은 이름으로 원격 디렉터리(LDAP 등)에서 객체를 찾아오는데, 그 응답이 다른 곳의 클래스를 불러오도록 가리킬 수 있었다. 설정 파일에서 쓰라고 만든 편의 기능이 로그로 들어온 바깥 데이터에도 적용된 것이 문제였다. 로그 한 줄이 "이 주소에서 코드를 가져와라"는 명령이 됐다.

Log4Shell 단계 그림: 공격자가 요청 헤더에 lookup 문자열을 넣어 보내고, 자바 앱이 그 값을 로그로 기록하면 Log4j가 lookup을 해석해 바깥 서버에 조회하고, 바깥 서버가 가리킨 클래스를 불러와 실행한다. 고친 판과 egress 제한은 조회 단계에서 사슬을 끊는다공격자요청만 보냄자바 앱받은 값을 기록Log4j 2로그 라이브러리바깥 서버공격자가 운영헤더에 ${jndi:…}값을 로그로 기록lookup: 원격 조회원격 클래스 로드서버 권한으로 코드 실행고친 판은 조회하지 않고, egress 제한은 연결을 막는다공격자가 고른 데이터의 길고친 판·egress 제한
1 / 5

공격자는 사용자 이름, 검색어, 요청 헤더처럼 서버가 로그에 남길 만한 값에 lookup 꼴의 글자(${jndi:…}, 주소는 비움)를 넣어 보낸다.

Log4Shell의 흐름. 공격자는 서버에 코드를 직접 올리지 않는다. 로그에 남을 값 한 줄로, 서버가 스스로 바깥에서 코드를 가져오게 만든다.

영향 범위와 고친 판

피해 범위가 컸던 까닭은 기술보다 위치에 있다. 거의 모든 자바 서버가 로그를 남기고, Log4j는 직접 쓰지 않아도 다른 라이브러리·제품을 통해 딸려 들어오는 전이 의존성(transitive dependency)인 경우가 많았다. 그래서 많은 조직이 "우리가 Log4j를 쓰는가"부터 답하지 못했다. CISA와 여러 나라 기관의 공동 권고 AA21-356A도 첫 순서로 Log4j를 쓰는 자산의 목록을 만들라고 했다.

고치는 일도 한 번에 끝나지 않았다. Apache Logging 보안 페이지의 기록을 따라가면 다음과 같다.

  • 2.15.0: 메시지 lookup을 기본으로 끔. 그러나 일부 기본값이 아닌 설정에서는 여전히 닿을 수 있었다(CVE-2021-45046, CVSS 9.0).
  • 2.16.0: 메시지 lookup 기능을 없애고 JNDI를 기본으로 끔. 이후 lookup이 자기 자신을 되부르는 무한 재귀로 프로세스가 죽는 문제(CVE-2021-45105, 서비스 거부)가 나왔다.
  • 2.17.0: 재귀 문제 수정. 마지막으로 공격자가 로깅 설정을 바꿀 수 있을 때의 JDBC Appender 문제(CVE-2021-44832)가 2.17.1에서 고쳐졌다.

Java 7·6 환경에는 2.12.x·2.3.x 줄에 따로 고친 판이 나왔다. 패치 전 임시 조치로는 서버의 바깥 방향 LDAP·RMI 연결을 막는 egress 제한이 권고됐다(AA21-356A).

배울 점

  • 로그도 입력을 처리하는 코드다. 로그에 쓰는 값을 해석하는 기능은 그 자체로 공격 표면이다.
  • 쓰는 줄도 몰랐던 부품이 가장 큰 구멍이 된다. 의존성 목록(SBOM)이 있어야 "우리가 영향받나"에 몇 분 안에 답한다.
  • 첫 패치가 끝이 아니다. 큰 사건은 고친 판이 연달아 나오므로 공지를 계속 따라가야 한다.
  • 서버가 바깥으로 아무 데나 접속할 수 없었다면 코드를 내려받는 단계에서 막혔다. egress 제한은 다음 사건에서도 통한다.

미국 사이버안전검토위원회(CSRB)는 2022년 7월 Log4j 사건 검토 보고서에서 취약한 Log4j가 앞으로도 오랫동안 시스템에 남을 것이라고 봤다.

Spring4Shell (2022, Spring Framework 데이터 바인딩)

무엇이 일어났나

2022년 3월 31일 Spring은 CVE-2022-22965를 공지했다. JDK 9 이상에서 돌아가는 Spring MVC·WebFlux 앱이 데이터 바인딩을 통해 원격 코드 실행에 노출될 수 있다는 내용이다. Log4Shell 넉 달 뒤라 "또 하나의 Log4Shell"이라는 뜻의 Spring4Shell이라는 별명이 붙었다. NVD 기준 CVSS 9.8이고, 2022년 4월 4일 KEV에 올랐다.

왜 그렇게 됐나

데이터 바인딩은 요청 매개변수를 자바 객체의 속성에 자동으로 채워 주는 기능이다. user.address.city 같은 이름은 점을 따라 속성의 속성까지 찾아간다. 개발자가 의도한 것은 폼 객체의 필드였지만, 모든 자바 객체에는 class라는 속성 경로가 붙어 있고 JDK 9에서 생긴 새 경로를 거치면 그 끝이 런타임 내부 객체까지 이어졌다. 바깥 요청이 어떤 속성을 바꿀지를 고를 수 있었다는 것이 핵심이다. Spring의 임시 조치가 바인딩에서 class로 시작하는 경로를 막는 것(disallowedFields)이었던 이유도 여기 있다.

Spring 데이터 바인딩의 속성 경로: 요청 매개변수 이름이 점으로 경로를 지정해 폼 객체에 값을 채운다. 의도한 길은 이름이나 이메일 같은 필드까지이고, class 속성을 거치면 런타임 내부 객체와 서버 설정까지 닿는다. 고친 판과 disallowedFields 설정이 이 갈래를 막는다요청 매개변수점(.)으로 경로 지정폼 객체바인딩 대상의도한 필드name·email 등class → 내부 객체런타임·서버 설정고친 판·disallowedFieldsJDK 9에서 class 경로 끝에 새 길이 생겼다
데이터 바인딩의 속성 경로. 의도한 길은 폼 객체의 필드까지다. 매개변수 이름이 class 속성을 거쳐 런타임 내부까지 내려가면 바깥 요청이 서버 내부 설정을 바꿀 수 있다. 고친 판과 임시 조치는 이 갈래를 막는다.

영향 범위와 고친 판 — 전제 조건을 읽는 법

이 사건은 공지의 전제 조건을 꼼꼼히 읽는 연습이 된다. Spring의 첫 공지는 알려진 공격이 성립하는 조건을 이렇게 적었다.

  • JDK 9 이상
  • Apache Tomcat에 WAR로 배포(Spring Boot의 기본인 실행형 jar는 이 공격에 해당하지 않음)
  • spring-webmvc 또는 spring-webflux 의존성
  • Spring Framework 5.3.0~5.3.17, 5.2.0~5.2.19 또는 그 이전

그래서 "우리는 jar로 배포하니 안전하다"고 결론 내리기 쉽지만, 같은 공지는 취약점의 성질이 더 일반적이라 다른 공격 경로가 있을 수 있다고 덧붙였다. 전제 조건은 "지금 알려진 공격"의 조건이지 "취약하지 않음"의 증명이 아니다. 고친 판은 Spring Framework 5.3.18·5.2.20이고, 이를 담은 Spring Boot 2.6.6·2.5.12가 함께 나왔다. 업그레이드가 어려운 곳에는 Tomcat 업그레이드, Java 8로 내리기, disallowedFields 설정이 임시 조치로 제시됐다.

배울 점

  • 자동 바인딩은 편하지만 "바깥이 서버 객체의 어느 칸을 쓸 수 있나"를 정하는 일이다. 받을 필드를 허용 목록으로 정한 전용 객체(DTO)를 쓰면 이런 종류의 문제를 피한다.
  • 런타임(JDK) 업그레이드가 오래된 코드에 새 길을 열 수 있다. 보안 검토는 코드만이 아니라 실행 환경 변화도 본다.
  • "우리 구성은 해당 없음"이라는 판단에도 패치는 한다. 전제 조건은 시간이 지나며 넓어지기도 한다.

Confluence OGNL 인젝션 (2022)

무엇이 일어났나

2022년 6월 2일 Atlassian은 보안 공지에서 Confluence Server와 Data Center의 인증 없는 원격 코드 실행 취약점 CVE-2022-26134를 알리며, 이미 실제 공격이 일어나고 있다고 밝혔다. 고친 판이 나오기 전에 공지가 먼저 나온 경우다. CISA는 공지 당일 이 취약점을 KEV에 올렸다. 로그인하지 않은 누구나, 인터넷에 열린 사내 위키 서버에서 코드를 실행할 수 있었다.

왜 그렇게 됐나

OGNL(Object-Graph Navigation Language)은 자바 객체의 속성을 읽고 메서드를 부르는 표현식 언어다. 템플릿이나 설정에서 값을 꺼내 쓰라고 만든 것인데, 요청에 실린 바깥 입력이 이 평가기에 식으로 들어가 계산됐다. 식을 평가할 수 있으면 메서드를 부를 수 있고, 메서드를 부를 수 있으면 프로그램을 실행할 수 있다. 표현식 언어 인젝션은 MITRE가 CWE-917로 분류한다.

입력을 값으로 다룰 때와 식으로 평가할 때: 값으로 다루면 입력은 글자로 저장되고 표시될 뿐이다. OGNL 같은 표현식 평가기에 들어가면 입력이 식으로 계산되어 메서드 호출과 프로그램 실행까지 이어진다값으로 다룰 때입력글자로 저장·표시결과: 그 글자 그대로식으로 평가할 때입력OGNL 평가기메서드 호출프로그램 실행표현식 언어 인젝션(CWE-917): 평가기에 바깥 입력을 넣지 않는다
입력을 값으로 다룰 때와 식으로 평가할 때. 위쪽은 입력이 끝까지 글자로 남는다. 아래쪽은 입력이 표현식 평가기에 들어가 객체 메서드 호출까지 이어진다.

영향 범위와 고친 판

공지는 지원 중인 모든 판이 영향을 받는다고 했고, NVD 기록은 1.3.0 이후 판을 범위로 적는다. 고친 판은 7.4.17, 7.13.7, 7.14.3, 7.15.2, 7.16.4, 7.17.4, 7.18.1이다. 업그레이드 전 임시 조치로 특정 jar 파일을 바꿔 넣는 방법이 나왔고, Atlassian이 운영하는 클라우드판은 해당하지 않았다. NVD 기준 CVSS 9.8이다.

배울 점

  • 표현식 평가기에는 바깥 입력을 넣지 않는다. 꼭 필요하면 쓸 수 있는 속성과 메서드를 좁힌 샌드박스 모드를 쓴다.
  • 인터넷에 그대로 열린 사내 도구는 공격자에게 가장 먼저 발견된다. 사내용은 VPN·접근 프록시 뒤에 둔다(제로 트러스트 노트).
  • 패치가 없는 날에도 할 일은 있다. 인터넷 노출 차단, 임시 조치, 침해 흔적 찾기를 먼저 한다.

자바 역직렬화의 뿌리 (2015, Commons Collections)

무엇이 일어났나

2015년 1월 28일 Gabriel Lawrence와 Chris Frohoff가 AppSec California에서, 자바 직렬화 데이터를 받는 서버가 흔한 라이브러리 Apache Commons Collections를 갖고만 있어도 원격 코드 실행에 노출된다는 것을 보였다. 그해 11월 6일에는 WebLogic, WebSphere, JBoss, Jenkins 같은 제품별 공격 시나리오가 공개돼 큰 소동이 났다. Commons Collections 보안 페이지에 따르면 라이브러리 자체에는 CVE가 붙지 않았고, 제품마다 CVE가 붙었다(예: WebLogic의 CVE-2015-4852).

왜 그렇게 됐나

역직렬화(deserialization)는 바이트 흐름에서 객체를 되살리는 일이다. 자바의 기본 직렬화는 바이트 안에 적힌 클래스 이름대로 객체를 만들고, 되살리는 도중에 그 클래스의 메서드를 부른다. 공격자는 새 코드를 보낼 필요가 없다. 서버의 클래스 경로에 이미 있는 클래스들을 골라, 하나가 되살아나며 부른 메서드가 다음 객체의 메서드를 부르도록 엮는다. 이 사슬을 가젯 체인(gadget chain)이라 부른다. Commons Collections에는 직렬화 가능하면서 리플렉션으로 임의 메서드를 부르는 클래스가 있어 사슬의 좋은 고리가 됐다.

역직렬화 필터: 필터가 없으면 바이트 흐름에 적힌 클래스가 그대로 되살아나고, 되살리는 중에 이미 있는 클래스들의 메서드가 사슬처럼 불려 실행까지 이어진다. 허용 목록 필터가 있으면 되살리기 전에 클래스 이름을 확인해 목록 안의 클래스만 객체로 만들고 나머지는 거부한다필터 없음: 되살리는 중에 메서드가 사슬처럼 불린다바이트 흐름그대로 되살림가젯 A가젯 B가젯 C실행허용 목록 필터(look-ahead)바이트 흐름클래스 이름 확인되살리기 전에목록 안: 객체로 만든다목록 밖: 거부한다예: 자바 JEP 290 직렬화 필터, Commons Collections 3.2.2의 기본 거부
역직렬화 필터. 위쪽처럼 클래스 이름을 확인하지 않으면 바이트에 적힌 어떤 클래스든 되살아나고, 되살리는 중에 불린 메서드들이 사슬처럼 이어진다. 아래쪽처럼 허용 목록에 있는 클래스만 받으면 사슬의 첫 고리부터 끊긴다.

영향 범위와 고친 판

Commons Collections 3.2.2는 위험한 클래스의 역직렬화를 기본으로 거부하게 했고, 4.1은 그 클래스들의 직렬화 지원을 아예 없앴다(영향 판 3.0~4.0). 하지만 Apache Commons 팀은 당시 성명에서 이 라이브러리만 고친다고 앱이 안전해지지 않는다고 못 박았다. 다른 라이브러리에도 가젯이 있기 때문이다. 문제는 특정 클래스가 아니라 믿을 수 없는 데이터를 역직렬화하는 설계였다.

배울 점

  • 믿을 수 없는 쪽에서 온 데이터는 언어 고유 직렬화(Java 직렬화, Python pickle 등)로 되살리지 않는다. JSON처럼 데이터만 담는 형식을 쓰고, 받은 뒤 스키마로 검증한다(CWE-502).
  • 꼭 써야 하면 되살리기 전에 클래스 이름을 확인하는 look-ahead 검사와 허용 목록을 둔다. 자바는 JEP 290으로 이를 위한 직렬화 필터를 표준에 넣었다.
  • 차단 목록은 새 가젯이 나올 때마다 뚫린다. 허용 목록이 기본이다. 자세한 언어별 지침은 OWASP 역직렬화 치트 시트에 있다.

AI 모델 파일에서 같은 문제가 다시 나타난 이야기는 AI 보안 노트의 pickle 절에 있다. 10년 뒤 JavaScript 쪽에서 되풀이된 사례는 아래 React Server Components 절에서 본다.

PHP-CGI 인자 주입 (2024, CVE-2024-4577)

무엇이 일어났나

2024년 6월 6일 PHP 8.3.8·8.2.20·8.1.29가 나오며 CVE-2024-4577을 고쳤다. Windows에서 Apache와 PHP-CGI를 함께 쓰는 서버에서, 바깥 요청이 PHP 실행 파일에 명령줄 옵션을 넘겨 소스 코드를 드러내거나 임의 PHP 코드를 실행하게 할 수 있었다. NVD 기준 CVSS 9.8이고, 2024년 6월 12일 KEV에 올랐다. PHP의 보안 공지 제목은 "CVE-2012-1823의 우회"다.

왜 그렇게 됐나

뿌리는 2012년의 CVE-2012-1823이다. CGI 방식에서는 웹 서버가 요청의 쿼리 문자열 일부를 php-cgi의 명령줄 인자로 넘기는 경우가 있어, 하이픈(-)으로 시작하는 값이 PHP 옵션으로 읽혔다. 당시 수정은 "하이픈으로 시작하면 옵션으로 보지 않는다"는 검사였다.

2024년의 우회는 문자 인코딩에서 나왔다. Windows는 유니코드 문자를 옛 코드 페이지로 바꿀 때 딱 맞는 글자가 없으면 비슷한 글자로 대신하는 Best-Fit 변환을 한다. 일본어(932)·간체 중국어(936)·번체 중국어(950) 코드 페이지에서는 소프트 하이픈 같은 글자가 보통 하이픈으로 바뀌었다. PHP의 검사는 변환 전 글자를 보고 통과시켰는데, 실제로 실행되는 명령줄은 변환 후 글자를 받았다. 검사한 값과 쓰인 값이 달랐다.

PHP-CGI Best-Fit 우회의 원리: 요청의 쿼리 문자열에 하이픈을 닮은 글자가 오면 PHP의 검사는 하이픈으로 시작하지 않는다고 보고 통과시킨다. Windows가 명령줄을 넘기며 일본어와 중국어 코드 페이지의 Best-Fit 변환으로 그 글자를 하이픈으로 바꾸고, php-cgi는 이를 명령줄 옵션으로 받아들인다. 검사한 값과 실제로 쓰인 값이 다르다검사한 값(변환 전)실제로 쓰인 값(변환 후)쿼리 문자열하이픈 닮은 글자(소프트 하이픈 등)PHP의 검사'-'로 시작하나?아니오 → 통과WindowsBest-Fit 변환닮은 글자 → '-'php-cgi'-…'를 옵션으로받아들인다검사는 변환 전 글자를, 실행은 변환 후 글자를 봤다영향: Windows + PHP-CGI + 코드 페이지 932·936·950(일본어·중국어)
검사와 사용 사이의 변환. PHP는 하이픈이 아닌 글자를 보고 통과시키지만, Windows가 명령줄을 넘기며 특정 코드 페이지에서 그 글자를 하이픈으로 바꾼다. php-cgi는 결국 옵션을 받는다.

영향 범위와 고친 판

영향은 좁지만 깊었다. Windows용 PHP만, PHP를 CGI 방식으로 돌리는 구성만, 그리고 시스템 로캘이 일본·중국·홍콩·마카오·싱가포르·대만 쪽인 서버가 대상이었다. 다만 PHP 공지에 실린 제보자의 설명에 따르면 개발용으로 널리 쓰는 XAMPP의 Windows 기본 설치가 그대로 해당했다. 고친 판은 8.3.8·8.2.20·8.1.29이고, 이미 지원이 끝난 8.0·7·5 줄에는 패치가 나오지 않았다. 몇 달 뒤에는 비표준 코드 페이지 설정에서 이 수정마저 우회되는 CVE-2024-8926이 나와 8.3.12·8.2.24·8.1.30에서 다시 고쳐졌다.

배울 점

  • 검사는 실제로 쓰이는 형태에서 한다. 인코딩 변환·정규화·디코딩이 검사 뒤에 일어나면 검사는 의미를 잃는다.
  • 입력을 다른 프로그램의 명령줄로 넘기는 구조(CWE-88, 인자 주입) 자체를 피한다. PHP라면 CGI 대신 FastCGI(PHP-FPM)나 모듈 방식을 쓴다.
  • "개발용 패키지"가 인터넷에 열려 운영되는 경우를 늘 의심한다. 지원이 끝난 판은 고쳐지지 않는다.

React Server Components·Next.js RCE (2025)

무엇이 일어났나

2025년 12월 3일 React 팀은 공지에서 React Server Components(RSC)의 인증 없는 원격 코드 실행 취약점 CVE-2025-55182를 알렸다. CVSS 10.0이고, CISA는 이틀 뒤인 12월 5일 KEV에 올렸다. 같은 날 Next.js도 자기 쪽 영향을 CVE-2025-66478로 공지했는데, NVD는 이 번호를 CVE-2025-55182의 중복으로 거부 처리했다. 같은 결함을 두 번호로 부른 셈이다.

왜 그렇게 됐나

RSC에서는 브라우저가 서버 쪽 함수(Server Function)를 부를 때, 인자를 React 고유 형식으로 직렬화해 요청 본문에 실어 보낸다. 서버는 이 본문을 객체로 되살린 뒤 함수를 부른다. React 공지는 취약한 코드가 Server Function 엔드포인트로 온 HTTP 요청의 내용을 안전하지 않게 역직렬화했다고 설명한다. 2015년 자바 사건과 같은 갈래가 10년 뒤 JavaScript 프레임워크에서 되풀이된 것이다. 두 공지 모두 아직 패치하지 못한 곳을 위해 기술적 세부 사항을 일부러 줄였다.

React Server Components 단계 그림: 브라우저가 Server Function 요청을 보내면 서버가 요청 본문을 객체로 되살려 함수를 부른다. 조작한 요청 본문은 취약한 판에서 되살리는 과정에서 의도치 않은 실행 경로로 이어졌고, 고친 판은 되살리는 단계에서 거부한다브라우저누구나 보낼 수 있음서버(RSC)Next.js 등런타임서버 프로세스 권한Server Function 요청되살린 인자로 함수 호출[조작한 요청 본문]의도치 않은 실행 경로되살릴 때 거부조작한 요청의 길고친 판
1 / 4

브라우저가 서버 쪽 함수(Server Function)를 부를 때, 인자를 React 고유 형식으로 직렬화해 요청 본문에 실어 보낸다. 이 요청은 누구나 직접 만들어 보낼 수 있다.

React Server Components의 Server Function 요청. 프레임워크가 감춰 둔 통신 형식도 결국 바깥에서 온 바이트를 객체로 되살리는 역직렬화다.

영향 범위와 고친 판

  • React: react-server-dom-webpack·react-server-dom-parcel·react-server-dom-turbopack의 19.0.0, 19.1.0, 19.1.1, 19.2.0. 고친 판은 19.0.1, 19.1.2, 19.2.1이다.
  • Next.js: App Router를 쓰는 15.x, 16.x, 그리고 14.3.0-canary.77 이후 canary. 처음 고친 판은 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7이다. 13.x, 14.x 안정판, Pages Router, Edge Runtime은 해당하지 않았다.
  • React Router의 RSC 기능, Waku, Parcel·Vite의 RSC 플러그인, Redwood SDK 같은 다른 RSC 구현도 영향 목록에 올랐다.

눈여겨볼 점은 두 가지다. 첫째, React 공지는 Server Function을 직접 만들지 않았더라도 RSC를 지원하는 앱이면 취약할 수 있다고 했다. 개발자가 따로 무언가를 켜지 않은 기본 구성이 대상이었다. 둘째, 후속 문제가 이어졌다. 12월 11일 후속 공지가 서비스 거부와 소스 코드 노출 취약점을 알렸고, 그래서 위의 처음 고친 판보다 각 판 줄의 최신 패치로 올려야 한다. Next.js는 12월 4일 오후(태평양 시간)까지 패치 없이 온라인이던 앱에 쓰던 비밀 값(API 키 등)을 모두 바꾸라고 권했다.

배울 점

  • 프레임워크가 숨겨 둔 통신 형식도 역직렬화다. "우리는 eval을 안 쓴다"가 안전의 증명이 되지 않는다.
  • 코드 실행 취약점은 패치로 끝나지 않는다. 노출 기간 동안 비밀 값이 새었다고 보고 교체한다(침해 가정).
  • 호스팅 업체의 임시 차단 규칙은 시간을 벌어 줄 뿐이다. React 공지도 이것을 업그레이드 대신 믿지 말라고 했다.

비교: Next.js 미들웨어 인가 우회 (2025, CVE-2025-29927)

무엇이 일어났나

같은 해 3월 21일 공개된 CVE-2025-29927은 원격 코드 실행이 아니라 인가 우회다. 그래도 RCE와 나란히 볼 만하다. Next.js에서 로그인 확인 같은 인가 검사를 미들웨어에만 두었다면, 요청에 특정 내부용 헤더를 붙이는 것만으로 그 검사를 건너뛸 수 있었다. GitHub이 매긴 CVSS는 9.1이다.

왜 그렇게 됐나

Vercel의 사후 분석에 따르면, 미들웨어가 안에서 다시 앱을 부를 때 미들웨어가 끝없이 되불리는 것을 막으려고 Next.js는 x-middleware-subrequest라는 문서화되지 않은 내부 헤더를 썼다. 이 헤더가 있으면 "미들웨어는 이미 돌았다"고 믿었는데, 그 헤더가 바깥 요청에서 왔는지는 확인하지 않았다. 안쪽끼리만 쓰려던 신호를 바깥도 보낼 수 있었다.

Next.js 미들웨어 인가 우회: 보통 요청은 미들웨어의 인가 검사를 거쳐 보호된 페이지로 간다. 내부용 헤더가 붙은 요청은 미들웨어가 이미 돌았다고 여겨 검사를 건너뛰고 보호된 페이지로 간다. 앞단 프록시가 바깥에서 온 그 헤더를 지우면 요청은 다시 미들웨어 검사를 거친다보통 요청요청미들웨어 인가 검사보호된 페이지내부 헤더가 붙은 요청요청 + 내부 헤더검사 건너뜀보호된 페이지앞단 프록시가 헤더를 지울 때요청 + 내부 헤더프록시: 헤더 지움미들웨어 인가 검사페이지
내부 헤더를 믿을 때. 보통 요청은 미들웨어의 인가 검사를 거친다. 내부용 헤더가 붙은 요청은 이미 검사를 마친 것으로 여겨져 곧장 페이지로 간다. 앞단 프록시가 바깥에서 온 이 헤더를 지우면 길이 막힌다.

영향 범위와 고친 판

영향 판은 11.1.4부터 12.3.5·13.5.9·14.2.25·15.2.3 직전까지이고, 고친 판은 12.3.5, 13.5.9, 14.2.25, 15.2.3이다. 사후 분석은 next start나 standalone 출력으로 직접 호스팅하며 미들웨어를 쓰는 앱이 영향을 받았고, Vercel·Netlify·Cloudflare Workers 호스팅과 정적 내보내기는 해당하지 않았다고 정리한다. 패치가 어려우면 바깥 요청의 그 헤더를 앞단에서 지우라는 것이 공지의 임시 조치였다.

배울 점

  • "내부에서만 쓰는 헤더"는 없다. 바깥과 같은 통로로 오는 신호는 신뢰 경계에서 지우거나 서명해 확인한다.
  • 인가는 한 겹에 몰지 않는다. 미들웨어는 1차 거름망이고, 데이터에 접근하는 서버 코드에서 다시 확인한다(인증·인가 노트).
  • RCE가 아니어도 점수가 높다. CVSS는 "무엇을 할 수 있나"를 재므로, 인증 없이 보호된 데이터에 닿으면 치명적이다.

대응의 공통 틀

사례들을 겹쳐 보면 대응 순서가 거의 같다. 한 겹이 뚫려도 다음 겹이 피해를 줄이도록 다섯 겹으로 생각한다.

RCE 대응의 다섯 겹: 찾기는 SBOM으로 그 부품을 쓰는지 답하고, 고치기는 KEV와 CVSS로 순서를 정해 패치하며, 버티기는 WAF 규칙과 설정 임시 조치로 패치 전 시간을 벌고, 줄이기는 최소 권한과 egress 제한과 샌드박스로 뚫려도 피해를 줄이며, 알아채기는 침해를 가정하고 흔적을 찾아 사고 대응으로 넘어간다1 찾기SBOM으로 그 부품을 쓰는지 바로 답한다기본2 고치기KEV·CVSS로 순서를 정해 패치한다기본3 버티기WAF 규칙·설정 임시 조치로 시간을 번다패치 전 임시4 줄이기최소 권한·egress 제한·샌드박스·격리뚫려도5 알아채기침해 가정, 흔적 찾기, 사고 대응뚫린 뒤
RCE 대응의 다섯 겹. 찾기와 고치기가 기본이고, 버티기는 패치 전의 임시 방편이다. 줄이기와 알아채기는 패치가 늦거나 이미 뚫린 경우를 위한 겹이다.

1. 찾기: 우리가 그 부품을 쓰는가

Log4Shell 때 가장 오래 걸린 일은 "어디에 Log4j가 있나"를 알아내는 것이었다. 직접 의존성뿐 아니라 전이 의존성, 사서 쓰는 제품 안의 부품까지 포함한 SBOM(소프트웨어 자재 명세서)이 있으면 공지가 뜬 날 바로 영향 여부를 판단한다. SBOM과 공급망 보안은 시스템·소프트웨어 보안 노트에 정리해 두었다.

2. 고치기: 패치 속도와 KEV

공개에서 실제 악용까지는 며칠이면 충분하다. 아래 그림은 이 글의 사례가 공개된 뒤 CISA의 알려진 악용 취약점(KEV) 목록에 오르기까지 걸린 날수다. KEV는 "실제로 공격에 쓰이고 있다"는 증거가 확인된 취약점만 올리므로, CVSS 점수와 함께 패치 순서를 정하는 가장 실용적인 기준이 된다. CISA는 2026년 6월 BOD 26-04로 연방 기관에 KEV 목록과 자산 노출 정도에 따라 패치 기한을 정하게 했다.

공개일부터 CISA KEV 등재까지 걸린 날수: Log4Shell 0일(같은 날), Spring4Shell 4일, Confluence OGNL 0일(같은 날), PHP-CGI 6일, React RSC 2일, Next.js 미들웨어 KEV 미등재사례공개 → KEV 등재(일)0일2일4일6일Log4Shell0일(같은 날)Spring4Shell4일Confluence OGNL0일(같은 날)PHP-CGI6일React RSC2일Next.js 미들웨어KEV 미등재
공개일부터 KEV 등재일까지 걸린 날수. 공개일은 각 프로젝트 공지(또는 NVD 게시일), 등재일은 NVD에 적힌 CISA KEV 날짜다. 대부분 일주일이 안 된다. Next.js 미들웨어 우회는 KEV에 오르지 않았다.

패치 주기, CVE·CVSS 읽는 법, 아직 KEV에 오르지 않은 취약점의 악용 가능성을 확률로 추정하는 EPSS는 사람과 운영 노트에 있다.

3. 버티기: WAF와 설정 임시 조치

WAF(웹 애플리케이션 방화벽) 규칙은 패치까지 시간을 버는 수단이다. Log4Shell에서는 같은 뜻의 문자열을 여러 겹으로 감추는 우회가 쏟아졌고, CISA 권고도 탐지 방법 하나에 기대지 말라고 했다. Spring의 disallowedFields, Confluence의 jar 교체, Next.js의 헤더 지우기도 모두 같은 자리다. 임시 조치를 했다면 그 사실과 되돌릴 날짜를 기록하고, 패치 후 걷어 낸다.

4. 줄이기: 뚫려도 덜 아프게

  • 최소 권한: 서버 프로세스를 관리자 권한으로 돌리지 않는다. 코드 실행의 피해는 그 프로세스의 권한만큼이다.
  • egress 제한: 앱 서버가 바깥 아무 곳에나 접속하지 못하게 한다. Log4Shell은 코드를 받아 오는 단계에서, 많은 공격은 훔친 데이터를 내보내는 단계에서 막힌다.
  • 샌드박스와 격리: 컨테이너·읽기 전용 파일 시스템·샌드박스로 실행 범위를 좁히고, 마이크로세그멘테이션으로 옆 시스템으로 번지는 길을 끊는다.
  • 비밀 값 분리: 오래가는 키를 환경 변수에 두기보다 짧게 사는 자격 증명을 쓰면, React 사례처럼 "모든 비밀을 바꾸라"는 일이 작아진다.

5. 알아채기: 탐지와 사고 대응

패치를 했어도 패치 전에 이미 뚫렸을 수 있다. CISA의 Log4j 권고는 취약했던 자산을 침해된 것으로 가정하고 격리한 뒤, 공개 전후의 설정 변경과 침해 흔적을 찾으라고 했다. 낯선 바깥 연결, 웹 서버 프로세스가 띄운 셸, 새로 생긴 계정과 예약 작업이 대표적인 흔적이다. 찾았다면 사고 대응 절차로 넘어간다(사람과 운영 노트의 사고 대응 절).

마지막으로 처음 그림으로 돌아가자. 새 기능을 설계할 때 "이 입력은 결국 무엇에 해석되나"를 묻는 습관이 가장 싼 대응이다. 역직렬화 대신 데이터 형식, 표현식 평가 대신 정해진 값, 동적 로딩 대신 허용 목록, 명령줄 대신 API를 고르면 이 글의 사례 대부분이 처음부터 생기지 않는다.