설명
신뢰할 수 없는 XML을 엔티티 확장이 가능한 상태로 파싱할 때, 공격자가 DTD 내부에 매우 깊이 중첩된 내부 엔티티를 정의하여 작은 입력으로도 기하급수적으로 큰 문자열을 생성하게 만드는 취약점입니다. 이로 인해 파서는 메모리와 CPU를 과도하게 소모하며 서비스 거부(DoS)가 발생합니다. node-expat 같은 파서를 사용할 때는 엔티티 확장 동작과 해당 버전의 자원 제한을 확인해야 합니다. 확장이 충분히 제한되지 않으면 빌리언 래프스 형태의 XML이 가용성을 해칠 수 있습니다.
잠재적 영향
- 서비스 거부(DoS): 과도한 엔티티 확장으로 프로세스가 응답 불가 상태가 됩니다.
- 메모리 고갈: 확장된 텍스트가 메모리를 점유하여 OOM(메모리 부족)로 프로세스가 종료될 수 있습니다.
- CPU 자원 고갈: 엔티티 치환/파싱 반복으로 CPU 사용률이 급상승합니다.
- 시스템 안정성 저하: 애플리케이션 충돌, 자동 재시작 루프, 장애 전파로 가용성이 떨어집니다.
- 비용 증가: 오토스케일링 환경에서 불필요한 리소스 확장과 비용이 발생할 수 있습니다.
해결 방법
- 엔티티 확장 비활성화: 가능한 경우 XML 파서 옵션에서 DTD/Entity 확장을 끄고, DOCTYPE 선언을 거부하세요.
- 안전한 파서 사용: node-expat처럼 내부 엔티티를 기본 확장하는 라이브러리 대신 sax 같은 스트리밍 파서(사용자 정의 엔티티를 확장하지 않음) 또는 엔티티 확장 제어가 가능한 파서를 사용하세요.
- 입력 제한: 요청 본문 크기(limit), 파싱 시간(timeout), 노드/깊이/텍스트 길이 제한을 적용하세요.
- 유효성 검증: 스키마(whitelist) 검증을 적용하고, 입력 내 <!DOCTYPE 등 DTD 관련 토큰이 발견되면 요청을 거부하세요.
- 방어적 구성: 리버스 프록시/웹서버에서 요청 크기 제한, 애플리케이션에서 작업 큐와 동시성 제한으로 폭주를 제한하세요.
예시
변경 전
javascript
const express = require("express");
const expat = require("node-expat");
const app = express();
app.use(express.text({ type: "application/xml" }));
app.post("/parse", (req, res) => {
const xml = req.body; // 신뢰할 수 없는 입력
const p = new expat.Parser();
p.on("startElement", () => {});
p.on("text", () => {});
// 취약: 내부 엔티티가 자동 확장되어 XML bomb에 취약
p.write(xml);
res.send("parsed");
});
module.exports = app;
변경 후
javascript
const express = require("express");
const sax = require("sax");
const app = express();
// 본문 크기 제한 등 선제적 방어
app.use(express.text({ type: "application/xml", limit: "200kb" }));
app.post("/safe-parse", (req, res) => {
const xml = req.body || "";
// DOCTYPE/ENTITY 등 DTD 관련 토큰이 보이면 거부
const suspicious = /<!DOCTYPE|<!ENTITY/i.test(xml);
if (suspicious) {
return res.status(400).send("DTD/ENTITY not allowed");
}
// sax는 사용자 정의 엔티티를 확장하지 않는 스트리밍 파서
const parser = sax.parser(true); // strict mode
parser.onopentag = () => {};
parser.ontext = () => {};
parser.onerror = (e) => {
throw e;
};
try {
parser.write(xml).close();
} catch {
return res.status(400).send("invalid xml");
}
return res.send("ok");
});
module.exports = app;
설명:
- 변경 전: 신뢰되지 않은 XML을 엔티티를 확장하는 파서에 전달합니다. 파서 버전과 자원 제한이 과도한 확장을 막지 못하면 작은 입력도 메모리와 CPU를 고갈시킬 수 있습니다.
- 변경 후: DOCTYPE/ENTITY가 포함된 XML을 사전에 차단해 DTD 기반 엔티티 확장을 원천 봉쇄합니다.
- sax 같은 스트리밍 파서는 사용자 정의 엔티티를 확장하지 않아 내부 엔티티 확장 공격에 강합니다.
- 요청 본문 크기 제한으로 대용량 입력을 차단하고, 오류가 나면 파싱을 중단하고 오류 응답을 한 번만 보냅니다.