mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 09:37:02 +00:00
Simple HTML sanitizer implementation
This commit is contained in:
9
sanitize/html.go
Normal file
9
sanitize/html.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package sanitize
|
||||
|
||||
import "github.com/microcosm-cc/bluemonday"
|
||||
|
||||
func HTML(html string) (output string, err error) {
|
||||
policy := bluemonday.UGCPolicy()
|
||||
output = policy.Sanitize(html)
|
||||
return
|
||||
}
|
||||
77
sanitize/html_test.go
Normal file
77
sanitize/html_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package sanitize_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jhillyerd/inbucket/sanitize"
|
||||
)
|
||||
|
||||
// TestHTMLPlainStrings test plain text passthrough
|
||||
func TestHTMLPlainStrings(t *testing.T) {
|
||||
testStrings := []string{
|
||||
"",
|
||||
"plain string",
|
||||
"one < two",
|
||||
}
|
||||
for _, ts := range testStrings {
|
||||
t.Run(ts, func(t *testing.T) {
|
||||
got, err := sanitize.HTML(ts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != ts {
|
||||
t.Errorf("Got: %q, want: %q", got, ts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTMLSimpleFormatting tests basic tags we should allow
|
||||
func TestHTMLSimpleFormatting(t *testing.T) {
|
||||
testStrings := []string{
|
||||
"<p>paragraph</p>",
|
||||
"<b>bold</b>",
|
||||
"<i>italic</b>",
|
||||
"<em>emphasis</em>",
|
||||
"<strong>strong</strong>",
|
||||
"<div><span>text</span></div>",
|
||||
}
|
||||
for _, ts := range testStrings {
|
||||
t.Run(ts, func(t *testing.T) {
|
||||
got, err := sanitize.HTML(ts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != ts {
|
||||
t.Errorf("Got: %q, want: %q", got, ts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTMLScriptTags tests some strings with JavaScript
|
||||
func TestHTMLScriptTags(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input, want string
|
||||
}{
|
||||
{
|
||||
`safe<script>nope</script>`,
|
||||
`safe`,
|
||||
},
|
||||
{
|
||||
`<a onblur="alert(something)" href="http://mysite.com">mysite</a>`,
|
||||
`<a href="http://mysite.com" rel="nofollow">mysite</a>`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
got, err := sanitize.HTML(tc.input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("Got: %q, want: %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -51,12 +51,17 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
#body-tabs > li > a {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.message-body {
|
||||
padding: 0 5px;
|
||||
padding: 10px 4px;
|
||||
}
|
||||
|
||||
.message-attachments {
|
||||
margin-top: 20px;
|
||||
margin-top: 5px;
|
||||
padding: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +157,7 @@ function onMessageLoaded(responseText, textStatus, XMLHttpRequest) {
|
||||
return;
|
||||
}
|
||||
onDocumentChange();
|
||||
$('#body-tabs a:first').tab('show')
|
||||
var top = $('#message-container').offset().top - navBarOffset;
|
||||
$(window).scrollTop(top);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
class="btn btn-primary"
|
||||
onClick="htmlView('{{.message.ID}}');">
|
||||
<span class="glyphicon glyphicon-new-window" aria-hidden="true"></span>
|
||||
HTML
|
||||
Raw HTML
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
@@ -78,7 +78,22 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="message-body">{{.body}}</div>
|
||||
<nav>
|
||||
<ul id="body-tabs" class="nav nav-tabs" role="tablist">
|
||||
{{if .htmlAvailable}}
|
||||
<li role="presentation">
|
||||
<a href="#body-html" aria-controls="body-html" role="tab" data-toggle="tab">Safe HTML</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li role="presentation">
|
||||
<a href="#body-text" aria-controls="body-text" role="tab" data-toggle="tab">Plain Text</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane message-body" id="body-html">{{.htmlBody}}</div>
|
||||
<div role="tabpanel" class="tab-pane message-body" id="body-text">{{.body}}</div>
|
||||
</div>
|
||||
|
||||
{{with .attachments}}
|
||||
<div class="well message-attachments">
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/jhillyerd/inbucket/datastore"
|
||||
"github.com/jhillyerd/inbucket/httpd"
|
||||
"github.com/jhillyerd/inbucket/log"
|
||||
"github.com/jhillyerd/inbucket/sanitize"
|
||||
"github.com/jhillyerd/inbucket/stringutil"
|
||||
)
|
||||
|
||||
@@ -118,6 +119,14 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
|
||||
}
|
||||
body := template.HTML(httpd.TextToHTML(mime.Text))
|
||||
htmlAvailable := mime.HTML != ""
|
||||
var htmlBody template.HTML
|
||||
if htmlAvailable {
|
||||
if str, err := sanitize.HTML(mime.HTML); err == nil {
|
||||
htmlBody = template.HTML(str)
|
||||
} else {
|
||||
log.Warnf("HTML sanitizer failed: %s", err)
|
||||
}
|
||||
}
|
||||
// Render partial template
|
||||
return httpd.RenderPartial("mailbox/_show.html", w, map[string]interface{}{
|
||||
"ctx": ctx,
|
||||
@@ -125,6 +134,7 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (
|
||||
"message": msg,
|
||||
"body": body,
|
||||
"htmlAvailable": htmlAvailable,
|
||||
"htmlBody": htmlBody,
|
||||
"mimeErrors": mime.Errors,
|
||||
"attachments": mime.Attachments,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user