project

CVE-2025-27415 1 day 취약점 분석

preo123 2025. 7. 8. 17:11

취약점

>=3.0.0nuxt<3.16.0에서 nuxt 서버 앞에 CDN이 있고 그 CDN이 URL 쿼리를 제외한 경로를 캐시에 저장할 때 http://mysite.com/?/_payload.json과 같이 쿼리 뒤에 _payload.json이 붙은 경로의 캐시 키가 http://mysite.com/로만 구성되어 다른 사용자가 http://mysite.com/에 요청을 보낼 때 캐시가 반환되어 정상적인 응답이 아닌 http://mysite.com/?/_payload.json의 응답이 반환되게 되는 취약점이다.

 


시나리오

1. http://mysite.com/?/_payload.json에 요청

2. CDN에 url query를 제외한 경로로 캐시키 생성 EX) http.mysite.com./

3. http://mysite.com/에 요청

4. CDN에 http://mysite.com/ 경로에 대한 캐시가 생성되어 있으므로 캐시에서 반환

5. http://mysite.com/?/_payload.json 페이지 반환

 


 

이를 통해 공격자가 캐시가 사라지는 주기마다 http://mysite.com/?/_payload.json와 같은 경로에 요청을 보내 캐시를 계속 생성시킨다면 다른 사용자가 페이지에 접속했을 때 정상적인 응답을 받을 수 없는 DoS 취약점이 발생할 수 있다.

 

https://nvd.nist.gov/vuln/detail/CVE-2025-27415

 

NVD - CVE-2025-27415

CVE-2025-27415 Detail Awaiting Analysis This CVE record has been marked for NVD enrichment efforts. Description Nuxt is an open-source web development framework for Vue.js. Prior to 3.16.0, by sending a crafted HTTP request to a server behind an CDN, it is

nvd.nist.gov

 


PoC

/** pages/index.vue */

<script setup>
definePageMeta({
  payload: () => {
    return { message: 'Hello payload' }
  }
})
</script>
<template>
  <h1>Hello from CVE-2025-27415</h1>
</template>

 

/** nginx.conf */

worker_processes 1;

events { worker_connections 1024; }

http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  proxy_cache_path /tmp/nginx-cache levels=1:2 keys_zone=my_cache:10m inactive=30s use_temp_path=off;

  server {
    listen 80;

    location / {
      proxy_pass http://localhost:3000;

      proxy_cache my_cache;
      proxy_cache_valid 200 1m;
      proxy_cache_key "$scheme$host$uri"; /** URL 쿼리를 제외한 경로가 캐시키로 저장됨 */
      add_header X-Cache-Status $upstream_cache_status;
    }
  }
}

 


환경 세팅

nvm install 20

nvm use 20


mkdir CVE-2025-27415


cd CVE-2025-27415

npx nuxi@3.15.0 init my-app


mkdir nginx

cd nginx

vi nginx.conf

sudo nginx -c $(pwd)/nginx.conf


cd my-app

npm install

mkdir pages

cd pages

vi index.vue


cd my-app

npm run build

npm run start

 

/** package.json */

{
  "name": "nuxt-app",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "start": "nuxt start"
  },
  "dependencies": {
    "nuxt": "3.15.0",
    "vue": "^3.4.21",
    "vue-router": "^4.2.5"
  }
}

 

github

https://github.com/jiseoung/CVE-2025-27415-PoC

 

GitHub - jiseoung/CVE-2025-27415-PoC: Nuxt3 Acceptance of Extraneous Untrusted Data With Trusted Data vulnerability

Nuxt3 Acceptance of Extraneous Untrusted Data With Trusted Data vulnerability - jiseoung/CVE-2025-27415-PoC

github.com


디버깅 환경

/** .vscode/launch.json */

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Nuxt Start: Debug Nitro",
      "program": "${workspaceFolder}/.output/server/index.mjs",
      "runtimeArgs": [
        "--inspect-brk"
      ],
      "cwd": "${workspaceFolder}",
      "skipFiles": ["<node_internals>/**"],
      "console": "integratedTerminal"
    }
  ]
}
/* nuxt.config.tx **/

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: '2025-05-15',
  devtools: { enabled: true },
  sourcemap: {
    server: true,
    client: true,
  },
})
npm run build
node --inspect-brk .output/server/index.mjs 
/* --inspect-brk로 인해 첫줄에서 멈춤
   npm run build로 인해 생성된 .output/server/index.mjs가 entry point **/

 


디버깅 방법

0. sudo nginx -c $(pwd)/nginx.conf (nginx 실행)
1. npm run build
2. node --inspect-brk .output/server/index.mjs
3. nitro.mjs 4886줄 중단점 (Nitro 서버 인스턴스 생성)

const nitroApp$1 = createNitroApp();


4. renderer.mjs 185줄 중단점 (_payload.json에 대한 요청인지 검증하는 코드의 시작점)

const PAYLOAD_URL_RE = /\/_payload.json(\?.*)?$/ ;

const isRenderingPayload = PAYLOAD_URL_RE.test(url) && !isRenderingIsland;

 

5. vscode Run and Debug 실행
6. http://localhost:8080/?/_payload.json으로 요청
7. http://localhost:8080/ 요청

    여기서는 디버거에 잡히지 않는데 이것이 응답이 nginx cache에서 반환되었다는 증거 

 


취약점 분석

nuxt3에서 서버를 빌드하고 실행할 때 봐야할 주요 파일이

,output/server/index.mjs

.output/server/chunks/nitro/nitro.mjs

.output/server/chunks/routes/renderer.mjs

이렇게 3개이다.

 

index.mjs가 entry point이고 nitro.mjs는 서버 실행, renderer.mjs는 페이지 렌더링을 하는 코드가 담겨 있다.

 

이 취약점은 페이지를 렌더링 하는 과정과 관련된 취약점이므로 renderer.mjs 파일을 보면 된다.

 

const componentIslands = false;

const PAYLOAD_URL_RE = /\/_payload.json(\?.*)?$/ ;

 

url이 '/?/_payload.json이므로 정규식에 매칭이 되고 isRenderingIsland의 값은 false이므로 isRederingPayload 변수는 true를 담게 된다.

 

그러므로 바로 아래에 있는 이 if문 내부가 실행되는데 각 변수가

이렇게 설정된다.

 

다음으로 더 아래인 234줄의 if문 내부가 실행되는데 

 

ssrContext 변수

SSR과정에 필요한 정보를 담는 ssrContext변수는 이렇게 설정되어 있고

renderPayloadResponse함수는

 ssrContext변수를 인자로 페이지 재사용 시 필요한 정보를 반환한다.

 

234줄의 if문에서 response2를 반환한 모습

 

그 후 캐시가 사라지기 전에 http://localhost:8080/로 요청을 보내면 디버거에 잡히지 않고

이렇게 응답이 반환되는데 이는 nginx의 캐시에서 반환된 응답이다.

 

http://localhost:8080/로 요청을 보냈을 때 원래 이렇게 index.vue 파일의 내용이 반환되어야 하는데 cache poisoning으로 인해 서로 다른 요청임에도 캐시에서 응답이 반환되었다.

 


취약점 원리

이 취약점은 nuxt3의 내부 라우팅 처리와 CDN 캐시키의 잘못된 설정이 얽혀 발생하는 취약점이다.

 

nuxt3에서 _payload.json으로 끝나는 URL에 요청을 보낼 시 위의 분석에서 본 것처럼 renderPayloadResponse(ssrContext) 함수를 사용해 SSR 페이지 요청과 분리되어 응답 처리가 이루어진다.

이때 /?/_payload.json으로 요청을 보낼 시 URL이 _payload.json으로 끝나므로 SSR 페이지 요청과 분리된 payload요청으로 처리하지만 CDN 캐시키를 uri까지만 저장되도록 해 ?뒤의 /_payload.json은 쿼리스트링의 키로 인식되어 /경로에 대한 캐시가 생성된다. 그러므로 클라이언트가 /경로에 대한 요청을 보냈을 때 CDN의 캐시가 반환되게 된다.

 

하지만 /_payload.json으로 요청을 보냈을 시에는 uri 자체가 /_payload.json이므로 /경로에 대한 캐시키 오염이 되지 않는 것이다.

 


_payload.json

nuxt3에서 SSR된 페이지를 클라이언트에서 다시 요청하면 클라이언트 단에서 그 페이지를 재사용(hydration)하게 된다.

이때 HTML만으로는 클라이언트가 페이지를 똑같이 만들어낼 수 없으므로 hook이나 state 등의 정보를 넘겨줘야 하는데 그 정보를 담고 있는 것이 _payload.json이다. 

그래서 페이지를 재사용할 때 클라이언트가 _payload.json으로 요청을 보내 정보를 가져온 후 페이지를 만들어낸다.

 

EX) /test에 대한 요청에서는 /test/_payload.json으로 요청을 보낸다.

 


취약점 패치

/?/_payload.json으로 요청을 보냈을 때

 

먼저 _payload.json으로 요청을 보내는 것인지 검증하는 정규식이 바뀌었다.

[^?]*이 추가됨으로써 쿼리스트링의 시작을 나타내는 '?' 앞이 _payload.json으로 끝나는지 검증한다.

 

취약한 버전에서는 url 전체를 검증하는 반면 패치된 버전에서는 ssrContext.url을 검증하는데

ssrContext는 createSSRContext함수의 반환값이고

createSSRContext함수를 보면

ssrContext.url = event.path이므로 '/?/_payload.json'으로 요청이 들어와도 path인 '/'가 검증된다.

디버거의 Watch에서 변수를 확인해보면 '/?/_payload.json'으로 요청했음에도 ssrContext.url에는 '/'가 들어가 있다.

 

그래서 isRederingPayload의 값을 확인해보면

false가 들어가 있어 _payload.json이 반환되지 않고

renderHTMLDocument함수가 실행되어 '/'에 대한 SSR html이 반환된다.