XML external entity (XXE) vulnerability

XML external entity (XXE) vulnerability

Description

Resolving external entities in untrusted XML can access local files or network resources. Despite its name, XMLParseNoEnt in github.com/lestrrat-go/libxml2/parser enables entity substitution. Impact depends on the libxml2 version, loader configuration, application permissions and use of the result.

Potential impact

  • Internal files readable by the parser may be exposed through results or errors.
  • An enabled network loader may send requests to internal services.
  • Entity expansion or large inputs may exhaust CPU or memory.

Remediation

  • Do not enable unnecessary entity substitution or external DTD loading for untrusted XML.
  • Verify that the installed Go binding and libxml2 configuration block external resource loading. Blocking network access alone does not block local file access.
  • Limit input size while reading and apply required schema validation. Schema or Content-Type validation does not replace blocking external entities.
  • Minimize file and network permissions, and use controlled tests to confirm that external resources are not read.

Examples

These HTTP examples require the Go binding and libxml2. Production authentication and TLS setup are omitted. The before-example's allocation from ContentLength and single Read also fail to handle unknown lengths and partial reads safely.

Before

go
package main

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

// Before: parse uploaded XML directly
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: enable entity substitution and external entity loading
    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)
}

After

go
package main

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

// Limit input size and omit the entity-substitution option
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) Do not enable substitution or external DTD loading
    p := parser.New() // No XMLParseNoEnt

    // 2) Perform required schema validation separately
    //    Schema validation does not replace blocking external resources

    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)
}

Explanation:

  • Before: XMLParseNoEnt enables entity substitution. Whether input such as the following file reference is resolved depends on the loader and permissions. Even if the file is read, exposure in a response depends on application logic.
xml
<!DOCTYPE d [<!ENTITY x SYSTEM "file:///etc/passwd">]>
<data>&x;</data>
  • After: The reader accepts at most one byte beyond 1 MiB and rejects oversized input. parser.New() receives no substitution or external DTD loading options. Also check that other loaders or global settings do not re-enable external access; input-size limits alone do not prevent every parsing resource-exhaustion issue.

References