XML 외부 개체(XXE) 엔티티 확장 허용

XML 엔티티 확장으로 인한 파일·네트워크 접근 위험

설명

libxmljs나 libxmljs2의 noent: true는 엔티티 참조를 실제 값으로 확장합니다. 신뢰하지 않는 XML이 외부 엔티티를 선언하고 파서가 이를 읽을 수 있다면 로컬 파일 접근이나 서버 측 요청 위조(SSRF)로 이어질 수 있습니다. 실제 파일·네트워크 접근과 확장 한도는 포함된 libxml2 버전, 빌드와 로더 설정에 따라 달라집니다.

잠재적 영향

  • 로컬 파일 노출: 외부 엔티티를 통해 애플리케이션이 파일 시스템(예: /etc/passwd)을 읽어 그 내용을 응답이나 로그로 유출할 수 있습니다.
  • SSRF(서버 측 요청 위조): 파서가 외부 엔티티를 해석하며 내부망/메타데이터 서비스(예: http://169.254.169.254)에 요청을 보내 민감 정보를 획득할 수 있습니다.
  • 서비스 거부(DoS): 거대한/재귀적 엔티티 확장(Billion Laughs)으로 CPU·메모리를 고갈시키고 서비스 중단을 일으킬 수 있습니다.

해결 방법

  • 신뢰하지 않는 XML에 noent를 켜지 말고 noent: false로 유지하세요.
  • DTD가 필요 없다면 거부하고, 외부 DTD·엔티티를 읽는 추가 옵션이나 사용자 정의 로더도 확인하세요.
  • XML의 미리 정의된 &, < 같은 참조는 파서가 처리합니다. 파싱한 텍스트를 HTML 엔티티 디코더로 다시 변환하지 마세요.
  • 입력 크기와 허용 형식을 제한하고 파싱 오류를 처리하세요. 확장량·메모리·실행 시간 제한도 확인하세요.
  • 네트워크 통신과 파일 접근 권한을 최소화하세요. noent: false 하나가 모든 외부 리소스 접근을 막는 것은 아닙니다.

예시

변경 전

javascript
const express = require('express');
const libxml = require('libxmljs');
const app = express();
app.use(express.text({ type: '*/*' }));

// 취약: 외부에서 받은 XML을 noent: true로 파싱
app.post('/api/parse-xml', (req, res) => {
  const xmlPayload = String(req.body || '');
  // noent: true -> 외부 엔티티까지 확장되어 파일 읽기/SSRF/DoS 가능
  const doc = libxml.parseXmlString(xmlPayload, { noent: true });
  res.json({ root: doc.root().name() });
});

app.listen(3000);

변경 후

javascript
const express = require('express');
const libxml = require('libxmljs');
const app = express();
app.use(express.text({ type: '*/*' }));

// 안전: noent 비활성화 + DTD 차단
app.post('/api/parse-xml', (req, res) => {
  const xmlPayload = String(req.body || '');

  // DTD가 있으면 거절 (외부 엔티티 선언 방지)
  if (/<!DOCTYPE/i.test(xmlPayload)) {
    return res.status(400).send('DTD is not allowed');
  }

  // noent 기본값 사용(또는 명시적으로 false)
  const doc = libxml.parseXmlString(xmlPayload, { noent: false });

  // 파서가 반환한 텍스트를 다시 디코딩하지 않음
  const textContent = doc.root().text();
  res.json({ root: doc.root().name(), text: textContent.slice(0, 200) });
});

app.listen(3000);

변경 전에는 엔티티 확장을 켭니다. 변경 후에는 Express가 문자열로 디코딩한 입력에서 DTD 선언을 거부하고 noent를 끕니다. 이 간단한 검사만으로 모든 XML 입력 경로를 보호한다고 가정하지 마세요. 사용 중인 파서의 외부 로딩 설정을 확인하고, 잘못된 XML에 대한 오류 응답을 추가하세요. 예시는 XML 텍스트를 그대로 사용하며 HTML 엔티티를 중복 디코딩하지 않습니다.

참조