frontend: fix XSS (#57) (#58)

This commit is contained in:
Yuhui Xu 2022-08-05 21:59:18 -04:00 committed by GitHub
parent 4b3980f6bd
commit 982326a678
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 56 additions and 38 deletions

View File

@ -6,7 +6,7 @@
<script src="/static/jsdelivr/npm/viz.js@2.1.2/lite.render.js" crossorigin="anonymous"></script> <script src="/static/jsdelivr/npm/viz.js@2.1.2/lite.render.js" crossorigin="anonymous"></script>
<script> <script>
var viz = new Viz(); var viz = new Viz();
viz.renderSVGElement(`{{ .Result }}`) viz.renderSVGElement(atob({{ .Result }}))
.then(element => { .then(element => {
document.getElementById("bgpmap").appendChild(element); document.getElementById("bgpmap").appendChild(element);
}) })

View File

@ -37,8 +37,8 @@
href="{{ .AllServersURLCustom }}"> {{ .AllServerTitle }} </a> href="{{ .AllServersURLCustom }}"> {{ .AllServerTitle }} </a>
{{ end }} {{ end }}
</li> </li>
{{ $length := len .ServersEscaped }} {{ $length := len .Servers }}
{{ range $k, $v := .ServersEscaped }} {{ range $k, $v := .Servers }}
<li class="nav-item"> <li class="nav-item">
{{ if gt $length 1 }} {{ if gt $length 1 }}
<a class="nav-link{{ if eq $server $v }} active{{ end }}" <a class="nav-link{{ if eq $server $v }} active{{ end }}"

View File

@ -4,12 +4,11 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"html/template"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"text/template"
) )
// static options map // static options map
@ -38,7 +37,7 @@ var summaryStateMap = map[string]string{
} }
// render the page template // render the page template
func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, content string) { func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, content template.HTML) {
path := r.URL.Path[1:] path := r.URL.Path[1:]
split := strings.SplitN(path, "/", 3) split := strings.SplitN(path, "/", 3)
@ -48,32 +47,26 @@ func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, co
// Use a default URL if the request URL is too short // Use a default URL if the request URL is too short
// The URL is for return to summary page // The URL is for return to summary page
if len(split) < 2 { if len(split) < 2 {
path = "summary/" + url.PathEscape(strings.Join(setting.servers, "+")) + "/" path = "summary/" + strings.Join(setting.servers, "+") + "/"
} else if len(split) == 2 { } else if len(split) == 2 {
path += "/" path += "/"
} }
split = strings.SplitN(path, "/", 3) split = strings.SplitN(path, "/", 3)
serversEscaped := make([]string, len(setting.servers))
for i, v := range setting.servers {
serversEscaped[i] = url.PathEscape(v)
}
args := TemplatePage{ args := TemplatePage{
Options: optionsMap, Options: optionsMap,
Servers: setting.servers, Servers: setting.servers,
ServersEscaped: serversEscaped,
ServersDisplay: setting.serversDisplay, ServersDisplay: setting.serversDisplay,
AllServersLinkActive: strings.ToLower(split[1]) == strings.ToLower(strings.Join(setting.servers, "+")), AllServersLinkActive: strings.EqualFold(split[1], strings.Join(setting.servers, "+")),
AllServersURL: url.PathEscape(strings.Join(setting.servers, "+")), AllServersURL: strings.Join(setting.servers, "+"),
AllServerTitle: setting.navBarAllServer, AllServerTitle: setting.navBarAllServer,
AllServersURLCustom: setting.navBarAllURL, AllServersURLCustom: setting.navBarAllURL,
IsWhois: isWhois, IsWhois: isWhois,
WhoisTarget: whoisTarget, WhoisTarget: whoisTarget,
URLOption: strings.ToLower(split[0]), URLOption: strings.ToLower(split[0]),
URLServer: url.PathEscape(strings.ToLower(split[1])), URLServer: strings.ToLower(split[1]),
URLCommand: split[2], URLCommand: split[2],
Title: setting.titleBrand + title, Title: setting.titleBrand + title,
Brand: setting.navBarBrand, Brand: setting.navBarBrand,
@ -91,7 +84,7 @@ func renderPageTemplate(w http.ResponseWriter, r *http.Request, title string, co
// Write the given text to http response, and add whois links for // Write the given text to http response, and add whois links for
// ASNs and IP addresses // ASNs and IP addresses
func smartFormatter(s string) string { func smartFormatter(s string) template.HTML {
var result string var result string
result += "<pre>" result += "<pre>"
s = template.HTMLEscapeString(s) s = template.HTMLEscapeString(s)
@ -108,7 +101,7 @@ func smartFormatter(s string) string {
result += lineFormatted + "\n" result += lineFormatted + "\n"
} }
result += "</pre>" result += "</pre>"
return result return template.HTML(result)
} }
// Parse bird show protocols result // Parse bird show protocols result
@ -200,11 +193,11 @@ func summaryParse(data string, serverName string) (TemplateSummary, error) {
} }
// Output a table for the summary page // Output a table for the summary page
func summaryTable(data string, serverName string) string { func summaryTable(data string, serverName string) template.HTML {
result, err := summaryParse(data, serverName) result, err := summaryParse(data, serverName)
if err != nil { if err != nil {
return "<pre>" + template.HTMLEscapeString(err.Error()) + "</pre>" return template.HTML("<pre>" + template.HTMLEscapeString(err.Error()) + "</pre>")
} }
// render the summary template // render the summary template
@ -215,5 +208,5 @@ func summaryTable(data string, serverName string) string {
fmt.Println("Error rendering summary:", err.Error()) fmt.Println("Error rendering summary:", err.Error())
} }
return buffer.String() return template.HTML(buffer.String())
} }

View File

@ -1,9 +1,9 @@
package main package main
import ( import (
"html/template"
"io/ioutil" "io/ioutil"
"net/http/httptest" "net/http/httptest"
"net/url"
"strings" "strings"
"testing" "testing"
) )
@ -25,7 +25,7 @@ func TestRenderPageTemplate(t *testing.T) {
r := httptest.NewRequest("GET", "/route/alpha/192.168.0.1/", nil) r := httptest.NewRequest("GET", "/route/alpha/192.168.0.1/", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
renderPageTemplate(w, r, title, content) renderPageTemplate(w, r, title, template.HTML(content))
resultBytes, _ := ioutil.ReadAll(w.Result().Body) resultBytes, _ := ioutil.ReadAll(w.Result().Body)
result := string(resultBytes) result := string(resultBytes)
@ -43,7 +43,27 @@ func TestRenderPageTemplateXSS(t *testing.T) {
evil := "<script>alert('evil');</script>" evil := "<script>alert('evil');</script>"
r := httptest.NewRequest("GET", "/whois/"+url.PathEscape(evil), nil) r := httptest.NewRequest("GET", "/whois/"+evil, nil)
w := httptest.NewRecorder()
// renderPageTemplate doesn't escape content, filter is done beforehand
renderPageTemplate(w, r, evil, "Test Content")
resultBytes, _ := ioutil.ReadAll(w.Result().Body)
result := string(resultBytes)
if strings.Contains(result, evil) {
t.Errorf("XSS injection succeeded: %s", result)
}
}
// https://github.com/xddxdd/bird-lg-go/issues/57
func TestRenderPageTemplateXSS_2(t *testing.T) {
initSettings()
evil := "<script>alert('evil');</script>"
r := httptest.NewRequest("GET", "/generic/dummy_server/"+evil, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
// renderPageTemplate doesn't escape content, filter is done beforehand // renderPageTemplate doesn't escape content, filter is done beforehand
@ -59,7 +79,7 @@ func TestRenderPageTemplateXSS(t *testing.T) {
func TestSmartFormatterXSS(t *testing.T) { func TestSmartFormatterXSS(t *testing.T) {
evil := "<script>alert('evil');</script>" evil := "<script>alert('evil');</script>"
result := smartFormatter(evil) result := string(smartFormatter(evil))
if strings.Contains(result, evil) { if strings.Contains(result, evil) {
t.Errorf("XSS injection succeeded: %s", result) t.Errorf("XSS injection succeeded: %s", result)
@ -71,7 +91,7 @@ func TestSummaryTableXSS(t *testing.T) {
evilData := `Name Proto Table State Since Info evilData := `Name Proto Table State Since Info
` + evil + ` ` + evil + ` --- up 2021-01-04 17:21:44 ` + evil ` + evil + ` ` + evil + ` --- up 2021-01-04 17:21:44 ` + evil
result := summaryTable(evilData, evil) result := string(summaryTable(evilData, evil))
if strings.Contains(result, evil) { if strings.Contains(result, evil) {
t.Errorf("XSS injection succeeded: %s", result) t.Errorf("XSS injection succeeded: %s", result)
@ -91,7 +111,7 @@ kernel2 Kernel master4 up 2021-08-27
direct1 Direct --- up 2021-08-27 direct1 Direct --- up 2021-08-27
int_babel Babel --- up 2021-08-27 ` int_babel Babel --- up 2021-08-27 `
result := summaryTable(data, "testserver") result := string(summaryTable(data, "testserver"))
expectedInclude := []string{"static1", "static2", "int_babel", "direct1"} expectedInclude := []string{"static1", "static2", "int_babel", "direct1"}
expectedExclude := []string{"device1", "kernel1", "kernel2"} expectedExclude := []string{"device1", "kernel1", "kernel2"}
@ -124,7 +144,7 @@ kernel2 Kernel master4 up 2021-08-27
direct1 Direct --- up 2021-08-27 direct1 Direct --- up 2021-08-27
int_babel Babel --- up 2021-08-27 ` int_babel Babel --- up 2021-08-27 `
result := summaryTable(data, "testserver") result := string(summaryTable(data, "testserver"))
expectedInclude := []string{"device1", "kernel1", "kernel2", "direct1", "int_babel"} expectedInclude := []string{"device1", "kernel1", "kernel2", "direct1", "int_babel"}
expectedExclude := []string{"static1", "static2"} expectedExclude := []string{"static1", "static2"}

View File

@ -2,8 +2,8 @@ package main
import ( import (
"embed" "embed"
"html/template"
"strings" "strings"
"text/template"
) )
// import templates and other assets // import templates and other assets
@ -19,7 +19,6 @@ type TemplatePage struct {
// Global options // Global options
Options map[string]string Options map[string]string
Servers []string Servers []string
ServersEscaped []string
ServersDisplay []string ServersDisplay []string
// Parameters related to current request // Parameters related to current request
@ -40,7 +39,7 @@ type TemplatePage struct {
Title string Title string
Brand string Brand string
BrandURL string BrandURL string
Content string Content template.HTML
} }
// summary // summary
@ -74,7 +73,7 @@ type TemplateSummary struct {
// whois // whois
type TemplateWhois struct { type TemplateWhois struct {
Target string Target string
Result string Result template.HTML
} }
// bgpmap // bgpmap
@ -88,7 +87,7 @@ type TemplateBGPmap struct {
type TemplateBird struct { type TemplateBird struct {
ServerName string ServerName string
Target string Target string
Result string Result template.HTML
} }
// global variable to hold the templates // global variable to hold the templates

View File

@ -2,8 +2,10 @@ package main
import ( import (
"bytes" "bytes"
"encoding/base64"
"fmt" "fmt"
"html" "html"
"html/template"
"io/fs" "io/fs"
"net" "net"
"net/http" "net/http"
@ -56,7 +58,7 @@ func webHandlerWhois(w http.ResponseWriter, r *http.Request) {
renderPageTemplate( renderPageTemplate(
w, r, w, r,
" - whois "+html.EscapeString(target), " - whois "+html.EscapeString(target),
buffer.String(), template.HTML(buffer.String()),
) )
} }
@ -89,7 +91,7 @@ func webBackendCommunicator(endpoint string, command string) func(w http.Respons
var content string var content string
for i, response := range responses { for i, response := range responses {
var result string var result template.HTML
if (endpoint == "bird") && backendCommand == "show protocols" && len(response) > 4 && strings.ToLower(response[0:4]) == "name" { if (endpoint == "bird") && backendCommand == "show protocols" && len(response) > 4 && strings.ToLower(response[0:4]) == "name" {
result = summaryTable(response, servers[i]) result = summaryTable(response, servers[i])
} else { } else {
@ -124,7 +126,7 @@ func webBackendCommunicator(endpoint string, command string) func(w http.Respons
renderPageTemplate( renderPageTemplate(
w, r, w, r,
" - "+endpoint+" "+backendCommand, " - "+endpoint+" "+backendCommand,
content, template.HTML(content),
) )
} }
} }
@ -154,11 +156,15 @@ func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWrite
var servers []string = strings.Split(split[1], "+") var servers []string = strings.Split(split[1], "+")
var responses []string = batchRequest(servers, endpoint, backendCommand) var responses []string = batchRequest(servers, endpoint, backendCommand)
// encode result with base64 to prevent xss
result := birdRouteToGraphviz(servers, responses, urlCommands)
result = base64.StdEncoding.EncodeToString([]byte(result))
// render the bgpmap result template // render the bgpmap result template
args := TemplateBGPmap{ args := TemplateBGPmap{
Servers: servers, Servers: servers,
Target: backendCommand, Target: backendCommand,
Result: birdRouteToGraphviz(servers, responses, urlCommands), Result: result,
} }
tmpl := TemplateLibrary["bgpmap"] tmpl := TemplateLibrary["bgpmap"]
@ -171,7 +177,7 @@ func webHandlerBGPMap(endpoint string, command string) func(w http.ResponseWrite
renderPageTemplate( renderPageTemplate(
w, r, w, r,
" - "+html.EscapeString(endpoint+" "+backendCommand), " - "+html.EscapeString(endpoint+" "+backendCommand),
buffer.String(), template.HTML(buffer.String()),
) )
} }
} }