XXEs in Golang are surprisingly hard

Go is one of my favourite programming languages, and I’ve enjoyed working with it while levelling up my skills in application security. Out of curiosity, I wanted to find out – how easily can we cause an XML External Entity (XXE) attack in a Golang application? As it turns out, doing so is surprisingly hard.

I won’t dive too deeply into how XXE attacks work, as there’s plenty of material available on that. As a quick recap, though:

  • As a language feature, XML allows us to define a key-value mapping called an entity, which our parser uses to do string substitution on instances of that entity within a document.
  • An external entity works similarly, but the parser will load content from an external URI (this includes filesystem contents and results of HTTP calls) when doing its string substitution.
  • A poorly configured XML parser will read and use an external entity definition from an untrusted input, allowing an attacker to gain access to sensitive data on the filesystem (such as /etc/passwd, or any other file the parser has permission to read).

With that, let’s begin!

Finding a working payload

Let’s start by finding a payload that we know works in another programming language’s XML parser. Take the simple Ruby script below (note that we need to explicitly enable external entities with Nokogiri::XML::ParseOptions::NOENT):

Run it and show the first ten lines of output:

$ ruby xxe.rb | head -n 10
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY users SYSTEM "file:///etc/passwd">
]>
<root>
    <child>##
# User Database
# 
# Note that this file is consulted directly only when the system is running
# in single-user mode.  At other times this information is provided by

Success! Within the <child> element, we see the header of my /etc/passwd file, so this is a working XXE payload. Let’s try using it in Go.

First attempt

Now that we’ve verified our payload in a Ruby script, let’s try doing the same thing in Go. We’ll use encoding/xml, Go’s built-in XML parsing library.

Run it, and we get… 

$ go run firstattempt.go
XML parsed into struct: {&users;}

Okay, so Decoder simply treated &users; as a string literal. That’s not what we want, but what if we tried setting Decoder.strict = true?

$ go run firstattempt.go
Error: XML syntax error on line 5: invalid character entity &users;
exit status 1

This is slightly better – we tried to parse &users; as an entity, but Decoder doesn’t recognize it as a valid entity. Why is that?

Writing to Decoder.Entity

If we peek into the docs of encoding/xml, we see that Decoder has a property called .Entity. It’s a map[string]string type that lets us define custom entities:

Let’s try setting .Entity. As an aside, this alone would make it difficult to carry out an XXE attack – a developer would have needed to explicitly set the .Entity map while coding their application – but, as we’ll see, there’s a lot more standing in an attacker’s way. We’ll modify our source code to write to .Entity:

Run our modified program:

$ go run entitymap.go 
{SYSTEM 'file:///etc/passwd'}

Okay, so Decoder did the substitution, but the file path is being treated as a string literal. Our parser isn’t fetching an external resource as we expect it to.

Reading encoding/xml’s source

The encoding/xml library is pretty small, so let’s dive into it! We’re able to find out pretty quickly while searching for entity and entities that encoding/xml doesn’t do much with our entity map. In fact, this is the only reference to it:

After this, we don’t see any calls to os.Open(), http.Get(), or anything else that would allow us to fetch an external resource. A simple string substitution is all that this library does with our .Entity map.

Confirmation via dtrace

Our source code tells us that we’re not opening /etc/passwd, but let’s double check this with dtrace! Well, I’ll be using dtruss, a similar tool for macOS. By viewing the system calls that our program makes, we should be able to tell if /etc/passwd is being read by our parser.

$ go build entitymap.go && sudo dtruss ./entitymap  2>&1 | grep passwd
XML parsed into struct: {SYSTEM 'file:///etc/passwd'}
write(0x1, "XML parsed into struct: {SYSTEM 'file:///etc/passwd'}\n\0", 0x36)            = 54 0

$ go build entitymap.go && sudo dtruss ./entitymap  2>&1 | grep open  
open("/dev/dtracehelper\0", 0x2, 0xFFFFFFFFEFBFF040)             = 3 0
open("/dev/urandom\0", 0x0, 0x0)                 = 3 0

Grepping for passwd, we just see that we write out our string literal from our entity map, and don’t actually open the file. Grepping again for open confirms this.

Conclusion

We can’t carry out an XXE attack on Golang applications using encoding/xml, since that library doesn’t handle external entities according to the XML language specification! The docs for encoding/xml describe it as “a simple XML 1.0 parser,” which sort of implies this, but I couldn’t find any docs that explicitly call out the lack of external entity processing.


It’s unclear to me whether the designers of Golang made this decision from an application security standpoint, or if they simply decided that it wouldn’t be worth the developer time to implement this XML language feature. While I find this design decision surprising, I do agree with it – the OWASP Top 10 recommends turning off external entity processing by default, and most XML documents don’t deal with external entities.


Of course, some Golang apps can still be vulnerable to XXE. You can easily find Go bindings for libxml2, which is a full-featured XML parser and has support for external entities. This is why XXEs in Golang are merely surprisingly hard, rather than impossible 🙂 But, by default, most developers will use the built-in encoding/xml library, which makes the entire ecosystem more secure.

Start your journey towards writing better software, and watch this space for new content.