Post

Go html/template iframe srcdoc XSS

Go html/template iframe srcdoc XSS

I reported an issue in Go html/template where interpolating untrusted input into iframe srcdoc can still lead to XSS.

The short version is that html/template applies normal HTML attribute escaping, but srcdoc is special. The browser first entity-decodes the attribute value and then parses the decoded string again as a full HTML document. That means a single escaping pass is not enough for this sink.

The Go security team replied that they are handling it publicly rather than through the private security policy, and opened a public issue here:

If an application does something like:

1
template.Must(template.New("").Parse(`<iframe srcdoc=""></iframe>`))

and attacker-controlled input reaches ``, script execution inside the iframe document is possible.

Affected Pattern

Minimal vulnerable example:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
	"html/template"
	"os"
)

func main() {
	t := template.Must(template.New("").Parse(
		`<iframe srcdoc=""></iframe>`))
	t.Execute(os.Stdout, `<script>alert(document.domain)</script>`)
}

Output:

1
<iframe srcdoc="&lt;script&gt;alert(document.domain)&lt;/script&gt;"></iframe>

At first glance this looks escaped, but in the browser this is still a valid XSS vector.

Why This Happens

html/template does contextual escaping. In this case it sees srcdoc as an HTML attribute and applies standard attribute escaping.

The problem is that srcdoc is not a normal text attribute. It is parsed in two stages:

  1. The outer HTML parser reads the srcdoc attribute value and decodes entities.
  2. The inner HTML parser parses the decoded value as the iframe document.

So for attacker input:

1
<script>alert(document.domain)</script>

the flow becomes:

Step 1: attribute escaping

1
&lt;script&gt;alert(document.domain)&lt;/script&gt;

Step 2: template output

1
<iframe srcdoc="&lt;script&gt;alert(document.domain)&lt;/script&gt;"></iframe>

Step 3: browser decodes the attribute value

1
<script>alert(document.domain)</script>

Step 4: browser parses the decoded content as the iframe document

The <script> element executes in the iframe context.

For this sink, safe interpolation requires encoding for both layers:

  • once for the inner HTML document
  • once again for the outer HTML attribute

Root Cause

The interesting part is that html/template already knows srcdoc contains HTML.

In attr.go, srcdoc is mapped as:

1
"srcdoc": contentTypeHTML,

But the tTag transition logic does not appear to have dedicated handling for contentTypeHTML in the same way it does for URL, CSS, JS, and srcset.

Conceptually it looks like:

1
2
3
4
5
6
7
8
9
10
11
switch attrType(attrName) {
case contentTypeURL:
	attr = attrURL
case contentTypeCSS:
	attr = attrStyle
case contentTypeJS:
	attr = attrScript
case contentTypeSrcset:
	attr = attrSrcset
// contentTypeHTML is not handled here
}

Because of that, srcdoc falls back to generic attribute escaping instead of a pipeline that accounts for the nested HTML parsing behavior.

Security Impact

This matters for applications that trust html/template to make this pattern safe automatically.

If attacker-controlled content is rendered into:

1
<iframe srcdoc="">

then a developer may believe the sink is safe because the output is entity-encoded. In practice, the browser decodes that value before parsing the iframe document, so script gadgets survive the first escaping layer.

Impact depends on how the iframe is used, but this can lead to:

  • script execution in the iframe document
  • attacker-controlled HTML in embedded content
  • trust boundary mistakes in applications relying on auto-escaping

Maintainer Response

I reported this privately to the Go security team and got the following response:

We’ve decided to treat this as a “security hardening” issue, which is handled outside of our Security Policy, and we have opened a public issue.

I disagree with that classification. From an application security point of view, this is still a valid XSS issue: a developer can use the standard safe-looking html/template pattern, get escaped output, and still end up with script execution because srcdoc is parsed twice by the browser.

Go security team reply

Mitigation

Do not pass untrusted input directly into srcdoc with html/template and assume normal auto-escaping is sufficient.

Safer options are:

  • avoid srcdoc for attacker-controlled content
  • sanitize the inner HTML with a dedicated HTML sanitizer
  • use a templating approach that accounts for nested HTML parsing
  • prefer safehtml/template if your use case fits its stricter model

Timeline

  • Report sent privately to Go security team
  • Maintainer response received
  • Public tracking issue opened: go.dev/issue/80154
This post is licensed under CC BY 4.0 by the author.