XML 외부 엔터티(XXE) 취약점

XML 외부 엔터티(XXE) 취약점

설명

XML 파서가 신뢰할 수 없는 문서의 외부 엔터티를 해석하면 로컬 파일이나 네트워크 자원에 접근할 수 있습니다. github.com/lestrrat-go/libxml2/parser의 XMLParseNoEnt는 이름과 달리 엔터티 치환을 켜는 옵션입니다. 실제 영향은 libxml2 버전, 로더 설정, 애플리케이션 권한과 결과 사용 방식에 따라 달라집니다.

잠재적 영향

  • 파서가 읽을 수 있는 내부 파일 내용이 처리 결과나 오류를 통해 노출될 수 있습니다.
  • 허용된 네트워크 로더를 통해 내부 서비스로 요청이 전송될 수 있습니다.
  • 엔터티 확장이나 큰 입력으로 CPU·메모리가 소진될 수 있습니다.

해결 방법

  • 신뢰할 수 없는 XML에는 불필요한 엔터티 치환과 외부 DTD 로드 옵션을 사용하지 마세요.
  • 설치된 Go 바인딩과 libxml2 버전에서 외부 리소스 로딩이 차단되는지 확인하세요. 네트워크 차단만으로 로컬 파일 접근까지 차단되는 것은 아닙니다.
  • 읽기 단계부터 입력 크기를 제한하고 필요한 스키마 검증을 적용하세요. 스키마나 Content-Type 검증은 외부 엔터티 차단을 대신하지 않습니다.
  • 파일·네트워크 권한을 최소화하고, 통제된 테스트로 외부 자원을 읽지 않는지 확인하세요.

예시

Go 바인딩과 libxml2가 설치된 HTTP 처리 예입니다. 운영 환경의 인증과 TLS 구성은 생략했습니다. 변경 전의 ContentLength 기반 할당과 한 번의 Read도 알 수 없는 길이·부분 읽기를 안전하게 처리하지 못합니다.

변경 전

go
package main

import (
    "fmt"
    "net/http"
    "github.com/lestrrat-go/libxml2/parser"
)

// 사용자가 업로드한 XML을 그대로 파싱하는 취약한 예제
func parseXMLUnsafe(w http.ResponseWriter, r *http.Request) {
    body := make([]byte, r.ContentLength)
    if _, err := r.Body.Read(body); err != nil {
        http.Error(w, "read error", http.StatusBadRequest)
        return
    }

    // CWE-611: 외부 엔터티 파싱을 허용하는 옵션 사용
    p := parser.New(parser.XMLParseNoEnt)

    doc, err := p.ParseString(string(body))
    if err != nil {
        http.Error(w, "parse error", http.StatusBadRequest)
        return
    }
    defer doc.Free()

    fmt.Fprintln(w, "XML parsed (unsafe)")
}

func main() {
    http.HandleFunc("/upload", parseXMLUnsafe)
    http.ListenAndServe(":8080", nil)
}

변경 후

go
package main

import (
    "fmt"
    "io"
    "net/http"
    "github.com/lestrrat-go/libxml2/parser"
)

// 입력 크기를 제한하고 엔터티 치환 옵션을 사용하지 않는 예제
func parseXMLSafe(w http.ResponseWriter, r *http.Request) {
    const maxBody = 1 << 20
    body, err := io.ReadAll(io.LimitReader(r.Body, maxBody+1))
    if err != nil {
        http.Error(w, "read error", http.StatusBadRequest)
        return
    }

    if len(body) > maxBody {
        http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
        return
    }

    // 1) 엔터티 치환과 외부 DTD 로드 옵션을 지정하지 않음
    p := parser.New() // XMLParseNoEnt 미사용

    // 2) 필요한 스키마 검증은 별도로 수행
    //    스키마 검증은 외부 리소스 차단을 대신하지 않음

    doc, err := p.ParseString(string(body))
    if err != nil {
        http.Error(w, "parse error", http.StatusBadRequest)
        return
    }
    defer doc.Free()

    fmt.Fprintln(w, "XML parsed safely")
}

func main() {
    http.HandleFunc("/upload", parseXMLSafe)
    http.ListenAndServe(":8080", nil)
}

설명:

  • 변경 전: XMLParseNoEnt가 엔터티 치환을 허용합니다. 아래처럼 파일을 참조하는 입력의 실제 처리 여부는 로더와 권한에 따라 달라지며, 파일이 읽히더라도 응답으로 유출되는지는 애플리케이션 로직에 달려 있습니다.
xml
<!DOCTYPE d [<!ENTITY x SYSTEM "file:///etc/passwd">]>
<data>&x;</data>
  • 변경 후: 읽는 양을 1MiB보다 한 바이트 많은 범위로 제한하고, 초과 입력을 거부합니다. parser.New()에는 치환·외부 DTD 로드 옵션을 전달하지 않습니다. 다른 로더·전역 설정이 외부 접근을 다시 허용하지 않는지도 확인해야 하며, 입력 크기 제한만으로 모든 파싱 자원 소모를 막을 수는 없습니다.

참조