본문 바로가기
Java

DocumentBuilderFactory를 이용한 안전한 XML 파싱과 XXE Injection 방지하기

by daddydontsleep 2024. 11. 25.
728x90
728x90

사진: UnsplashSergio García

  # [Java XML] DocumentBuilderFactory를 이용한 안전한 XML 파싱과 XXE Injection 방지하기

안녕하세요! 오늘은 Java에서 XML을 안전하게 파싱하고 XXE(XML External Entity) Injection 공격을 방지하는 방법에 대해 알아보겠습니다.

목차

  1. DocumentBuilderFactory란?
  2. XML 파싱 기본 방법
  3. XXE Injection이란?
  4. 안전한 XML 파싱 구현하기
  5. 실전 예제 코드

1. DocumentBuilderFactory란?

DocumentBuilderFactory는 Java에서 제공하는 XML 파싱을 위한 기본 클래스입니다. XML 문서를 파싱하여 DOM(Document Object Model) 객체로 변환하는 역할을 합니다.

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dbf.newDocumentBuilder();
Document doc = builder.parse(inputStream);

2. XML 파싱 기본 방법

2.1 XML 속성 추출하기

// XML 예시
<user id="123">
    <name>John Doe</name>
    <email>john@example.com</email>
</user>

// Java 코드
Element element = (Element) node;
String userId = element.getAttribute("id"); // "123" 반환

2.2 XML 요소 값 추출하기

// 텍스트 컨텐츠 가져오기
String name = element.getElementsByTagName("name")
                    .item(0)
                    .getTextContent(); // "John Doe" 반환

// NodeList를 이용한 반복 처리
NodeList nodeList = doc.getElementsByTagName("user");
for (int i = 0; i < nodeList.getLength(); i++) {
    Node node = nodeList.item(i);
    if (node.getNodeType() == Node.ELEMENT_NODE) {
        Element element = (Element) node;
        // 처리 로직
    }
}

3. XXE Injection이란?

XXE Injection은 XML 외부 개체를 이용한 보안 취약점 공격 방식입니다. 공격자가 XML 입력을 통해 서버의 파일을 읽거나, 서비스 거부 공격을 실행할 수 있습니다.

3.1 XXE 공격 예시

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [
    <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
    <data>&xxe;</data>
</root>

이러한 공격은 서버의 중요 파일을 노출시킬 수 있으며, 심각한 보안 위험을 초래할 수 있습니다.

4. 안전한 XML 파싱 구현하기

4.1 보안 설정

public class SecureXMLParser {
    private DocumentBuilderFactory createSecureDocumentBuilderFactory() {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        try {
            // XXE 방지를 위한 보안 설정
            dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
            dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            dbf.setExpandEntityReferences(false);
            dbf.setXIncludeAware(false);
            dbf.setValidating(false);

            return dbf;
        } catch (ParserConfigurationException e) {
            throw new RuntimeException("XML 파서 구성 오류", e);
        }
    }
}

4.2 주요 보안 설정 설명

설정 설명
FEATURE_SECURE_PROCESSING 안전한 XML 처리 활성화
disallow-doctype-decl DTD 사용 비활성화
external-general-entities 외부 일반 엔티티 비활성화
external-parameter-entities 외부 파라미터 엔티티 비활성화
setExpandEntityReferences 엔티티 참조 확장 비활성화

5. 실전 예제 코드

5.1 완성된 보안 XML 파서

public class SecureXMLParser {
    private static final Logger logger = LogManager.getLogger(SecureXMLParser.class);

    public Document parseXML(InputStream input) {
        try {
            DocumentBuilderFactory dbf = createSecureDocumentBuilderFactory();
            DocumentBuilder builder = dbf.newDocumentBuilder();

            // 에러 핸들러 설정
            builder.setErrorHandler(new ErrorHandler() {
                @Override
                public void warning(SAXParseException e) {
                    logger.warn("XML 파싱 경고: " + e.getMessage());
                }

                @Override
                public void error(SAXParseException e) throws SAXException {
                    throw e;
                }

                @Override
                public void fatalError(SAXParseException e) throws SAXException {
                    throw e;
                }
            });

            return builder.parse(input);

        } catch (Exception e) {
            logger.error("XML 파싱 중 오류 발생", e);
            throw new RuntimeException("XML 파싱 실패", e);
        }
    }

    // XML 데이터 추출 예제
    public UserData extractUserData(Document doc) {
        UserData userData = new UserData();

        NodeList userNodes = doc.getElementsByTagName("user");
        for (int i = 0; i < userNodes.getLength(); i++) {
            Node node = userNodes.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Element element = (Element) node;

                userData.setId(element.getAttribute("id"));
                userData.setName(getElementText(element, "name"));
                userData.setEmail(getElementText(element, "email"));
            }
        }

        return userData;
    }

    private String getElementText(Element parent, String tagName) {
        NodeList nodeList = parent.getElementsByTagName(tagName);
        if (nodeList.getLength() > 0) {
            return nodeList.item(0).getTextContent();
        }
        return null;
    }
}

5.2 사용 예제

public class XMLParserExample {
    public static void main(String[] args) {
        SecureXMLParser parser = new SecureXMLParser();

        try (InputStream is = new FileInputStream("user_data.xml")) {
            Document doc = parser.parseXML(is);
            UserData userData = parser.extractUserData(doc);

            System.out.println("사용자 ID: " + userData.getId());
            System.out.println("이름: " + userData.getName());
            System.out.println("이메일: " + userData.getEmail());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

보안 체크리스트

  • DTD 비활성화
  • 외부 엔티티 비활성화
  • 보안 프로세싱 활성화
  • 에러 핸들링 구현
  • 로깅 구현
  • 입력 검증

마치며

XML 파싱은 매우 유용하지만, 보안에 신경 쓰지 않으면 심각한 취약점이 될 수 있습니다. 이 글에서 설명한 보안 설정들을 반드시 적용하고, 정기적인 보안 검토를 통해 새로운 취약점에 대비해야 합니다.

참고 자료


본 글이 XML 파싱과 보안에 대한 이해를 높이는 데 도움이 되었기를 바랍니다. 궁금한 점이나 추가적인 의견이 있으시다면 댓글로 남겨주세요! 😊

#Java #XML #보안 #XXE #DocumentBuilderFactory #웹보안

728x90
300x250