mirror of
https://github.com/trezor/blockbook.git
synced 2026-02-19 16:31:19 +01:00
feat: add CSP headers and fix XSS vulnerabilities in templates
This commit is contained in:
committed by
elizaveta timofeeva
parent
0954a540eb
commit
a20c7611a2
@@ -17,6 +17,25 @@ import (
|
||||
"github.com/trezor/blockbook/common"
|
||||
)
|
||||
|
||||
// getContentSecurityPolicy returns a Content Security Policy header value
|
||||
// to help prevent XSS attacks by controlling which resources can be loaded.
|
||||
//
|
||||
// Note: Uses 'unsafe-inline' for scripts and styles due to inline QRCode initialization
|
||||
// and Bootstrap requirements. Consider migrating to nonces for better security.
|
||||
func getContentSecurityPolicy() string {
|
||||
return "default-src 'self'; " +
|
||||
"script-src 'self' 'unsafe-inline'; " +
|
||||
"style-src 'self' 'unsafe-inline'; " +
|
||||
"img-src 'self' data: https: ipfs: https://ipfs.io; " +
|
||||
"connect-src 'self' https: ipfs: https://ipfs.io; " +
|
||||
"font-src 'self' data:; " +
|
||||
"object-src 'none'; " +
|
||||
"frame-ancestors 'none'; " +
|
||||
"base-uri 'self'; " +
|
||||
"form-action 'self'; " +
|
||||
"upgrade-insecure-requests;"
|
||||
}
|
||||
|
||||
type tpl int
|
||||
|
||||
const (
|
||||
@@ -56,6 +75,7 @@ func (s *htmlTemplates[TD]) jsonHandler(handler func(r *http.Request, apiVersion
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Content-Security-Policy", getContentSecurityPolicy())
|
||||
if e, isError := data.(jsonError); isError {
|
||||
w.WriteHeader(e.HTTPStatus)
|
||||
}
|
||||
@@ -116,6 +136,7 @@ func (s *htmlTemplates[TD]) htmlTemplateHandler(handler func(w http.ResponseWrit
|
||||
// noTpl means the handler completely handled the request
|
||||
if t != noTpl {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Content-Security-Policy", getContentSecurityPolicy())
|
||||
// return 500 Internal Server Error with errorInternalTpl
|
||||
if t == errorInternalTpl {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/trezor/blockbook/api"
|
||||
)
|
||||
|
||||
func Test_formatInt64(t *testing.T) {
|
||||
@@ -255,3 +257,140 @@ 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
|
||||
wantNotContains string // substring that must NOT be present (raw XSS payload)
|
||||
}{
|
||||
{
|
||||
name: "no alias",
|
||||
address: "0x1234567890123456789012345678901234567890",
|
||||
td: &TemplateData{},
|
||||
want: `<span class="copyable">0x1234567890123456789012345678901234567890</span>`,
|
||||
},
|
||||
{
|
||||
name: "normal alias",
|
||||
address: "0x1234567890123456789012345678901234567890",
|
||||
td: &TemplateData{
|
||||
Tx: &api.Tx{
|
||||
AddressAliases: api.AddressAliasesMap{
|
||||
"0x1234567890123456789012345678901234567890": api.AddressAlias{
|
||||
Type: "Contract",
|
||||
Alias: "MyContract",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `<span class="copyable" cc="0x1234567890123456789012345678901234567890" alias-type="Contract">MyContract</span>`,
|
||||
},
|
||||
{
|
||||
name: "XSS in alias.Type - quote injection",
|
||||
address: "0x1234567890123456789012345678901234567890",
|
||||
td: &TemplateData{
|
||||
Tx: &api.Tx{
|
||||
AddressAliases: api.AddressAliasesMap{
|
||||
"0x1234567890123456789012345678901234567890": api.AddressAlias{
|
||||
Type: `Contract" onclick="alert(1)" data="`,
|
||||
Alias: "MyContract",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantContains: `alias-type="Contract" onclick="alert(1)" data="`,
|
||||
wantNotContains: `onclick="alert(1)"`,
|
||||
},
|
||||
{
|
||||
name: "XSS in alias.Type - script tag",
|
||||
address: "0x1234567890123456789012345678901234567890",
|
||||
td: &TemplateData{
|
||||
Tx: &api.Tx{
|
||||
AddressAliases: api.AddressAliasesMap{
|
||||
"0x1234567890123456789012345678901234567890": api.AddressAlias{
|
||||
Type: `<script>alert(1)</script>`,
|
||||
Alias: "MyContract",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantContains: `alias-type="<script>alert(1)</script>"`,
|
||||
wantNotContains: `<script>`,
|
||||
},
|
||||
{
|
||||
name: "XSS in alias.Alias",
|
||||
address: "0x1234567890123456789012345678901234567890",
|
||||
td: &TemplateData{
|
||||
Tx: &api.Tx{
|
||||
AddressAliases: api.AddressAliasesMap{
|
||||
"0x1234567890123456789012345678901234567890": api.AddressAlias{
|
||||
Type: "Contract",
|
||||
Alias: `<img src=x onerror=alert(1)>`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantContains: `<img src=x onerror=alert(1)>`,
|
||||
wantNotContains: `<img src=x onerror=alert(1)>`,
|
||||
},
|
||||
{
|
||||
name: "XSS in address",
|
||||
address: `0x1234"><script>alert(1)</script>`,
|
||||
td: &TemplateData{
|
||||
Tx: &api.Tx{
|
||||
AddressAliases: api.AddressAliasesMap{
|
||||
`0x1234"><script>alert(1)</script>`: api.AddressAlias{
|
||||
Type: "Contract",
|
||||
Alias: "MyContract",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantContains: `cc="0x1234"><script>alert(1)</script>"`,
|
||||
wantNotContains: `<script>alert(1)</script>`,
|
||||
},
|
||||
{
|
||||
name: "XSS payload from real-world example",
|
||||
address: "0x1234567890123456789012345678901234567890",
|
||||
td: &TemplateData{
|
||||
Tx: &api.Tx{
|
||||
AddressAliases: api.AddressAliasesMap{
|
||||
"0x1234567890123456789012345678901234567890": api.AddressAlias{
|
||||
Type: `Contract" onmouseover="alert('XSS')" data="`,
|
||||
Alias: "NormalName",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantContains: `alias-type="Contract" onmouseover="alert('XSS')" data="`,
|
||||
wantNotContains: `onmouseover="alert('XSS')"`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,12 +702,12 @@ func addressAliasSpan(a string, td *TemplateData) template.HTML {
|
||||
alias := getAddressAlias(a, td)
|
||||
if alias == nil {
|
||||
rv.WriteString(`<span class="copyable">`)
|
||||
rv.WriteString(a)
|
||||
rv.WriteString(html.EscapeString(a))
|
||||
} else {
|
||||
rv.WriteString(`<span class="copyable" cc="`)
|
||||
rv.WriteString(html.EscapeString(a))
|
||||
rv.WriteString(`" alias-type="`)
|
||||
rv.WriteString(alias.Type)
|
||||
rv.WriteString(html.EscapeString(alias.Type))
|
||||
rv.WriteString(`">`)
|
||||
rv.WriteString(html.EscapeString(alias.Alias))
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
|
||||
status: http.StatusOK,
|
||||
contentType: "text/html; charset=utf-8",
|
||||
body: []string{
|
||||
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,shrink-to-fit=no"><link rel="stylesheet" href="/static/css/bootstrap.5.2.2.min.css"><link rel="stylesheet" href="/static/css/main.min.4.css"><script>var hasSecondary=false;</script><script src="/static/js/bootstrap.bundle.5.2.2.min.js"></script><script src="/static/js/main.min.4.js"></script><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="description" content="Trezor Fake Coin Explorer"><title>Trezor Fake Coin Explorer</title></head><body><header id="header"><nav class="navbar navbar-expand-lg"><div class="container"><a class="navbar-brand" href="/" title="Home"><span class="trezor-logo"></span><span style="padding-left: 140px;">Fake Coin Explorer</span></a><button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbarSupportedContent"><ul class="navbar-nav m-md-auto"><li class="nav-item pe-xl-4"><a href="/blocks" class="nav-link">Blocks</a></li><li class="nav-item"><a href="/" class="nav-link">Status</a></li></ul><span class="navbar-form"><form class="d-flex" id="search" action="/search" method="get"><input name="q" type="text" class="form-control form-control-lg" placeholder="Search for block, transaction, address or xpub" focus="true"><button class="btn" type="submit"><span class="search-icon"></span></button></form></span></div></div></nav></header><main id="wrap"><div class="container"><div class="row"><div class="col-md-10 order-2 order-md-1"><h1>XPUB</h1><h5 class="col-12 d-flex h-data pb-2"><span class="ellipsis copyable">tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej</span></h5><h4 class="row"><div class="col-lg-6"><span class="copyable">0 FAKE</span></div></h4></div><div class="col-md-2 order-1 order-md-2 d-flex justify-content-center justify-content-md-end mb-3 mb-md-0"><div id="qrcode"></div><script type="text/javascript" src="/static/js/qrcode.min.js"></script><script type="text/javascript">new QRCode(document.getElementById("qrcode"), { text: "tr([5c9e228d\/86\u0027\/1\u0027\/0\u0027]tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN\/{0,1}\/*)#4rqwxvej", width: 120, height: 120 });</script></div></div><table class="table data-table info-table"><tbody><tr><td style="white-space: nowrap;"><h5>Confirmed</h5></td><td></td></tr><tr><td style="width: 25%;">Total Received</td><td><span class="amt copyable" cc="0 FAKE"><span class="prim-amt">0 FAKE</span></span></td></tr><tr><td>Total Sent</td><td><span class="amt copyable" cc="0 FAKE"><span class="prim-amt">0 FAKE</span></span></td></tr><tr><td>Final Balance</td><td><span class="amt copyable" cc="0 FAKE"><span class="prim-amt">0 FAKE</span></span></td></tr><tr><td>No. Transactions</td><td>0</td></tr><tr><td>Used XPUB Addresses</td><td>0</td></tr></tbody></table><table class="table data-table"><tbody><tr><td style="white-space: nowrap; width: 50%;"><h5>XPUB Addresses with Balance</h5></td><td colspan="3"></td></tr><tr><td colspan="4">No addresses</td></tr></tbody></table><div class="row mb-4"><div class="col-12"><a href="?tokens=used" class="ms-3 me-3">Show used XPUB addresses</a><a href="?tokens=derived">Show all derived XPUB addresses</a></div></div></div></main><footer id="footer"><div class="container"><nav class="navbar navbar-dark"><span class="navbar-nav"><a class="nav-link" href="https://satoshilabs.com/" target="_blank" rel="noopener noreferrer">Created by SatoshiLabs</a></span><span class="navbar-nav ml-md-auto"><a class="nav-link" href="https://trezor.io/terms-of-use" target="_blank" rel="noopener noreferrer">Terms of Use</a></span><span class="navbar-nav ml-md-auto d-md-flex d-none"><a class="nav-link" href="https://trezor.io/" target="_blank" rel="noopener noreferrer">Trezor</a></span><span class="navbar-nav ml-md-auto d-md-flex d-none"><a class="nav-link" href="https://trezor.io/trezor-suite" target="_blank" rel="noopener noreferrer">Suite</a></span><span class="navbar-nav ml-md-auto d-md-flex d-none"><a class="nav-link" href="https://trezor.io/support" target="_blank" rel="noopener noreferrer">Support</a></span><span class="navbar-nav ml-md-auto"><a class="nav-link" href="/sendtx">Send Transaction</a></span><span class="navbar-nav ml-md-auto d-lg-flex d-none"><a class="nav-link" href="https://trezor.io/compare" target="_blank" rel="noopener noreferrer">Don't have a Trezor? Get one!</a></span></nav></div></footer></body></html>`,
|
||||
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,shrink-to-fit=no"><link rel="stylesheet" href="/static/css/bootstrap.5.2.2.min.css"><link rel="stylesheet" href="/static/css/main.min.4.css"><script>var hasSecondary=false;</script><script src="/static/js/bootstrap.bundle.5.2.2.min.js"></script><script src="/static/js/main.min.4.js"></script><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="description" content="Trezor Fake Coin Explorer"><title>Trezor Fake Coin Explorer</title></head><body><header id="header"><nav class="navbar navbar-expand-lg"><div class="container"><a class="navbar-brand" href="/" title="Home"><span class="trezor-logo"></span><span style="padding-left: 140px;">Fake Coin Explorer</span></a><button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbarSupportedContent"><ul class="navbar-nav m-md-auto"><li class="nav-item pe-xl-4"><a href="/blocks" class="nav-link">Blocks</a></li><li class="nav-item"><a href="/" class="nav-link">Status</a></li></ul><span class="navbar-form"><form class="d-flex" id="search" action="/search" method="get"><input name="q" type="text" class="form-control form-control-lg" placeholder="Search for block, transaction, address or xpub" focus="true"><button class="btn" type="submit"><span class="search-icon"></span></button></form></span></div></div></nav></header><main id="wrap"><div class="container"><div class="row"><div class="col-md-10 order-2 order-md-1"><h1>XPUB</h1><h5 class="col-12 d-flex h-data pb-2"><span class="ellipsis copyable">tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej</span></h5><h4 class="row"><div class="col-lg-6"><span class="copyable">0 FAKE</span></div></h4></div><div class="col-md-2 order-1 order-md-2 d-flex justify-content-center justify-content-md-end mb-3 mb-md-0"><div id="qrcode"></div><script type="text/javascript" src="/static/js/qrcode.min.js"></script><script type="text/javascript">new QRCode(document.getElementById("qrcode"), { text: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej", width: 120, height: 120 });</script></div></div><table class="table data-table info-table"><tbody><tr><td style="white-space: nowrap;"><h5>Confirmed</h5></td><td></td></tr><tr><td style="width: 25%;">Total Received</td><td><span class="amt copyable" cc="0 FAKE"><span class="prim-amt">0 FAKE</span></span></td></tr><tr><td>Total Sent</td><td><span class="amt copyable" cc="0 FAKE"><span class="prim-amt">0 FAKE</span></span></td></tr><tr><td>Final Balance</td><td><span class="amt copyable" cc="0 FAKE"><span class="prim-amt">0 FAKE</span></span></td></tr><tr><td>No. Transactions</td><td>0</td></tr><tr><td>Used XPUB Addresses</td><td>0</td></tr></tbody></table><table class="table data-table"><tbody><tr><td style="white-space: nowrap; width: 50%;"><h5>XPUB Addresses with Balance</h5></td><td colspan="3"></td></tr><tr><td colspan="4">No addresses</td></tr></tbody></table><div class="row mb-4"><div class="col-12"><a href="?tokens=used" class="ms-3 me-3">Show used XPUB addresses</a><a href="?tokens=derived">Show all derived XPUB addresses</a></div></div></div></main><footer id="footer"><div class="container"><nav class="navbar navbar-dark"><span class="navbar-nav"><a class="nav-link" href="https://satoshilabs.com/" target="_blank" rel="noopener noreferrer">Created by SatoshiLabs</a></span><span class="navbar-nav ml-md-auto"><a class="nav-link" href="https://trezor.io/terms-of-use" target="_blank" rel="noopener noreferrer">Terms of Use</a></span><span class="navbar-nav ml-md-auto d-md-flex d-none"><a class="nav-link" href="https://trezor.io/" target="_blank" rel="noopener noreferrer">Trezor</a></span><span class="navbar-nav ml-md-auto d-md-flex d-none"><a class="nav-link" href="https://trezor.io/trezor-suite" target="_blank" rel="noopener noreferrer">Suite</a></span><span class="navbar-nav ml-md-auto d-md-flex d-none"><a class="nav-link" href="https://trezor.io/support" target="_blank" rel="noopener noreferrer">Support</a></span><span class="navbar-nav ml-md-auto"><a class="nav-link" href="/sendtx">Send Transaction</a></span><span class="navbar-nav ml-md-auto d-lg-flex d-none"><a class="nav-link" href="https://trezor.io/compare" target="_blank" rel="noopener noreferrer">Don't have a Trezor? Get one!</a></span></nav></div></footer></body></html>`,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div id="qrcode"></div>
|
||||
<script type="text/javascript" src="/static/js/qrcode.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
new QRCode(document.getElementById("qrcode"), { text: "{{$addr.AddrStr}}", width: 120, height: 120 });
|
||||
new QRCode(document.getElementById("qrcode"), { text: {{jsStr $addr.AddrStr}}, width: 120, height: 120 });
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div id="qrcode"></div>
|
||||
<script type="text/javascript" src="/static/js/qrcode.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
new QRCode(document.getElementById("qrcode"), { text: "{{$addr.AddrStr}}", width: 120, height: 120 });
|
||||
new QRCode(document.getElementById("qrcode"), { text: {{jsStr $addr.AddrStr}}, width: 120, height: 120 });
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user