
로컬 랩에서 oRPC의 OpenAPI 문서 생성 시 저장형 XSS를 재현합니다(CVE-2026-33331). 취약 버전과 패치 버전을 비교용으로 포함하며, 독립 실행형 PoC도 제공합니다.
orpc의 OpenAPI 문서 생성에서 발생하는 저장형 XSS에 대한 개념 증명 실습 환경
oRPC는 OpenAPI 스펙을 기반으로 API 문서 페이지를 생성합니다. v1.13.9 이전 버전에서는 HTML 인코딩 없이 JSON.stringify()를 사용하여 스펙을 <script> 태그에 직접 넣었습니다. 공격자가 제어할 수 있는 info.description과 같은 필드는 해당 태그를 조기에 닫고 스크립트를 실행할 수 있었습니다.
이 저장소는 버그를 재현할 수 있는 로컬 실습 환경과 비교용 패치 버전을 제공합니다.
| 필드 | 값 |
|---|---|
| CVE ID | CVE-2026-33331 |
| GHSA | GHSA-7f6v-3gx7-27q8 |
| CWE | CWE-79 (웹 페이지 생성 중 입력값의 부적절한 중화) |
| 제품 | middleapi/orpc |
| 영향 범위 | v1.13.9 미만의 모든 버전 |
| 패치 버전 | v1.13.9 |
| 발견자 | Abhay Kumar (@abhayclasher) |
packages/openapi/src/plugins/openapi-reference.ts의 취약한 코드는 스펙을 다음과 같이 렌더링합니다:
<script id="spec" type="application/json">${JSON.stringify(spec)}</script>
JSON.stringify()는 유효한 JSON을 생성하지만 < 또는 >를 이스케이프하지 않습니다. 브라우저는 <script type="application/json"> 내부에서도 </script>를 여전히 HTML 종료 태그로 처리합니다.
실행을 유발하는 info.description의 페이로드:
"</script><script>alert('XSS')</script>"
브라우저는 다음과 같이 인식합니다:
<script id="spec" type="application/json">{"info":{"description":"
</script><script>alert('XSS')</script>"}}
</script>
첫 번째 </script>가 블록을 닫습니다. 그 이후의 모든 내용은 인라인 JavaScript로 실행됩니다.
이는 애플리케이션이 사용자 제어 입력값으로 OpenAPI 스펙을 생성할 때 적용되며, 이는 oRPC가 라우터 정의를 통해 지원하는 정확한 사례입니다.
git clone https://github.com/abhayclasher/CVE-2026-33331.git
cd CVE-2026-33331/app
npm install
node server.js
git clone https://github.com/abhayclasher/CVE-2026-33331.git
cd CVE-2026-33331
docker compose up -d
서버가 시작된 후 다음 URL을 방문하세요:
| URL | 설명 |
|---|---|
http://localhost:3000/docs | 취약한 문서 페이지 — 로드 시 XSS 페이로드 실행 |
http://localhost:3000/docs/safe | v1.13.9 패치가 적용된 동일 페이지 |
http://localhost:3000/spec.json | 원본 OpenAPI 스펙 JSON |
취약한 버전은 페이지 로드 시 alert()을 실행하여 스크립트 실행을 확인합니다. 패치된 버전은 동일한 악성 스펙을 안전하게 렌더링합니다 — 페이로드가 일반 텍스트로 표시됩니다.
독립형 PoC도 실행할 수 있습니다:
cd poc
node exploit.js
이 명령은 포트 3000에서 전체 애플리케이션 컨텍스트 없이 동일한 버그를 시연하는 최소 서버를 실행합니다.
1. 공격자가 OpenAPI 스펙의 필드(예: description)를 제어
2. oRPC가 JSON.stringify(spec)로 문서 HTML 생성
3. 페이로드의 </script>가 <script> 태그를 조기에 닫음
4. 브라우저가 이후 내용을 인라인 JavaScript로 실행
┌─────────────────────────────────────────────────────┐
│ 1. 악성 스펙 생성: │
│ │
│ info.description = │
│ "</script><script>alert('XSS')</script>" │
│ │
│ 2. orpc가 스펙을 HTML에 삽입: │
│ │
│ <script id="spec" type="application/json"> │
│ {"info":{"description":"</script> <-- 태그 │
│ <script>alert('XSS')</script> <-- 페이로드 │
│ "}} │
│ </script> │
│ │
│ 3. 브라우저가 alert('XSS') 실행 │
└─────────────────────────────────────────────────────┘
버전 1.13.9는 JSON을 스크립트 태그에 삽입하기 전에 HTML에 민감한 문자를 유니코드 이스케이프로 대체하는 escapeJsonForHtml()을 도입했습니다:
const escapeJsonForHtml = (obj) => JSON.stringify(obj)
.replace(/&/g, '\\u0026')
.replace(/'/g, '\\u0027')
.replace(/</g, '\\u003C')
.replace(/>/g, '\\u003E')
.replace(/\//g, '\\u002F');
유니코드 이스케이프는 유효한 JSON으로 유지되기 때문에 작동합니다 — JSON.parse()가 원래 문자열을 재구성합니다 — 하지만 브라우저 파서에는 HTML처럼 보이지 않습니다.
패치는 또한 DOM dataset.config 속성에서 JSON을 읽는 방식(값에 괄호가 포함된 경우 깨지는 문제)에서 이스케이프된 JSON을 인라인 변수에 직접 할당하는 방식으로 변경되었습니다.
전체 커밋 보기: 4f0efa8
CVE-2026-33331/
├── README.md # 이 파일
├── docker-compose.yml # Docker 실습 환경 구성
├── app/
│ ├── Dockerfile # 컨테이너 빌드 구성
│ ├── package.json # Node.js 의존성
│ └── server.js # 취약 + 패치된 문서 렌더러
└── poc/
└── exploit.js # 독립형 최소 개념 증명
Abhay Kumar이(가) 보고했습니다. 이 저장소는 로컬 교육용으로만 사용하도록 제작되었습니다. 취약한 서버를 공용 네트워크에 배포하지 마십시오.
교육 목적으로만 사용 — 격리된 환경에서 사용하세요