diff --git a/server/html_templates_test.go b/server/html_templates_test.go
index 6cf68b10..22b1b2cd 100644
--- a/server/html_templates_test.go
+++ b/server/html_templates_test.go
@@ -3,6 +3,7 @@
package server
import (
+ "bytes"
"html/template"
"reflect"
"strings"
@@ -10,6 +11,7 @@ import (
"time"
"github.com/trezor/blockbook/api"
+ "github.com/trezor/blockbook/bchain"
)
func Test_formatInt64(t *testing.T) {
@@ -260,11 +262,11 @@ func Test_appendAmountSpanBitcoinType(t *testing.T) {
func Test_addressAliasSpan_XSS(t *testing.T) {
tests := []struct {
- name string
- address string
- td *TemplateData
- want string
- wantContains string // substring that must be present and properly escaped
+ name string
+ address string
+ td *TemplateData
+ want string
+ wantContains string // substring that must be present and properly escaped
wantNotContains string // substring that must NOT be present (raw XSS payload)
}{
{
@@ -301,7 +303,7 @@ func Test_addressAliasSpan_XSS(t *testing.T) {
},
},
},
- wantContains: `alias-type="Contract" onclick="alert(1)" data="`,
+ wantContains: `alias-type="Contract" onclick="alert(1)" data="`,
wantNotContains: `onclick="alert(1)"`,
},
{
@@ -317,7 +319,7 @@ func Test_addressAliasSpan_XSS(t *testing.T) {
},
},
},
- wantContains: `alias-type="<script>alert(1)</script>"`,
+ wantContains: `alias-type="<script>alert(1)</script>"`,
wantNotContains: ``,
},
{
@@ -365,7 +367,7 @@ func Test_addressAliasSpan_XSS(t *testing.T) {
},
},
},
- wantContains: `alias-type="Contract" onmouseover="alert('XSS')" data="`,
+ wantContains: `alias-type="Contract" onmouseover="alert('XSS')" data="`,
wantNotContains: `onmouseover="alert('XSS')"`,
},
}
@@ -373,19 +375,19 @@ func Test_addressAliasSpan_XSS(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
got := addressAliasSpan(tt.address, tt.td)
gotStr := string(got)
-
+
if tt.want != "" {
if gotStr != tt.want {
t.Errorf("addressAliasSpan() = %v, want %v", gotStr, tt.want)
}
}
-
+
if tt.wantContains != "" {
if !strings.Contains(gotStr, tt.wantContains) {
t.Errorf("addressAliasSpan() = %v, should contain %v", gotStr, tt.wantContains)
}
}
-
+
if tt.wantNotContains != "" {
if strings.Contains(gotStr, tt.wantNotContains) {
t.Errorf("addressAliasSpan() = %v, should NOT contain raw XSS payload: %v", gotStr, tt.wantNotContains)
@@ -394,3 +396,49 @@ func Test_addressAliasSpan_XSS(t *testing.T) {
})
}
}
+
+func renderTokenDetailSpecific(t *testing.T, uri string) string {
+ t.Helper()
+
+ tmpl := template.Must(template.New("tokenDetail.html").Funcs(template.FuncMap{
+ "jsStr": jsStr,
+ }).ParseFiles("./static/templates/tokenDetail.html"))
+
+ data := TemplateData{
+ TokenId: "1",
+ URI: uri,
+ ContractInfo: &bchain.ContractInfo{
+ Contract: "0x1234567890123456789012345678901234567890",
+ Name: "Contract",
+ Standard: bchain.ERC771TokenStandard,
+ },
+ }
+
+ var rendered bytes.Buffer
+ if err := tmpl.ExecuteTemplate(&rendered, "specific", data); err != nil {
+ t.Fatalf("ExecuteTemplate() error = %v", err)
+ }
+ return rendered.String()
+}
+
+func Test_tokenDetailTemplateEscapesURIInJSContext(t *testing.T) {
+ body := renderTokenDetailSpecific(t, `";console.log("XSS_EXEC_OK");//`)
+
+ if !strings.Contains(body, `const uri="\";console.log(\"XSS_EXEC_OK\");//";`) {
+ t.Fatalf("escaped uri literal not found in output: %s", body)
+ }
+ if strings.Contains(body, `const uri="";console.log("XSS_EXEC_OK");//";`) {
+ t.Fatalf("found unescaped JS breakout payload in output: %s", body)
+ }
+}
+
+func Test_tokenDetailTemplateEscapesScriptEndTagInJSContext(t *testing.T) {
+ body := renderTokenDetailSpecific(t, `";//`)
+
+ if strings.Contains(body, ``) {
+ t.Fatalf("found unescaped script-end-tag payload in output: %s", body)
+ }
+ if !strings.Contains(body, `const uri="\";\u003c/script\u003e\u003cscript\u003ealert(1)\u003c/script\u003e//";`) {
+ t.Fatalf("escaped script-end-tag payload not found in output: %s", body)
+ }
+}
diff --git a/static/templates/tokenDetail.html b/static/templates/tokenDetail.html
index 9eec908b..65c2ca0b 100644
--- a/static/templates/tokenDetail.html
+++ b/static/templates/tokenDetail.html
@@ -51,7 +51,7 @@
}
async function getMetadata(url) {
try {
- const uri={{ jsStr $data.URI }};
+ const uri={{ $data.URI }};
if(uri) {
const response = await fetch(uri);
const contentType=response.headers.get('content-type');
@@ -89,4 +89,4 @@
}
getMetadata();
-{{end}}
\ No newline at end of file
+{{end}}