Merge pull request #22 from rtfb/master

Add some protection against script injection
This commit is contained in:
Vytautas Šaltenis 2013-05-21 13:19:17 -07:00
commit 2336fd3109
3 changed files with 110 additions and 20 deletions

80
html.go
View File

@ -28,6 +28,7 @@ const (
HTML_SKIP_STYLE // skip embedded <style> elements HTML_SKIP_STYLE // skip embedded <style> elements
HTML_SKIP_IMAGES // skip embedded images HTML_SKIP_IMAGES // skip embedded images
HTML_SKIP_LINKS // skip all links HTML_SKIP_LINKS // skip all links
HTML_SKIP_SCRIPT // skip embedded <script> elements
HTML_SAFELINK // only link to trusted protocols HTML_SAFELINK // only link to trusted protocols
HTML_TOC // generate a table of contents HTML_TOC // generate a table of contents
HTML_OMIT_CONTENTS // skip the main contents (for a standalone table of contents) HTML_OMIT_CONTENTS // skip the main contents (for a standalone table of contents)
@ -167,10 +168,32 @@ func (options *Html) BlockHtml(out *bytes.Buffer, text []byte) {
} }
doubleSpace(out) doubleSpace(out)
out.Write(text) if options.flags&HTML_SKIP_SCRIPT != 0 {
out.Write(stripTag(string(text), "script", "p"))
} else {
out.Write(text)
}
out.WriteByte('\n') out.WriteByte('\n')
} }
func stripTag(text, tag, newTag string) []byte {
closeNewTag := fmt.Sprintf("</%s>", newTag)
i := 0
for i < len(text) && text[i] != '<' {
i++
}
if i == len(text) {
return []byte(text)
}
found, end := findHtmlTagPos([]byte(text[i:]), tag)
closeTag := fmt.Sprintf("</%s>", tag)
noOpen := text
if found {
noOpen = text[0:i+1] + newTag + text[end:]
}
return []byte(strings.Replace(noOpen, closeTag, closeNewTag, -1))
}
func (options *Html) HRule(out *bytes.Buffer) { func (options *Html) HRule(out *bytes.Buffer) {
doubleSpace(out) doubleSpace(out)
out.WriteString("<hr") out.WriteString("<hr")
@ -460,6 +483,9 @@ func (options *Html) RawHtmlTag(out *bytes.Buffer, text []byte) {
if options.flags&HTML_SKIP_IMAGES != 0 && isHtmlTag(text, "img") { if options.flags&HTML_SKIP_IMAGES != 0 && isHtmlTag(text, "img") {
return return
} }
if options.flags&HTML_SKIP_SCRIPT != 0 && isHtmlTag(text, "script") {
return
}
out.Write(text) out.Write(text)
} }
@ -646,39 +672,65 @@ func (options *Html) TocFinalize() {
} }
func isHtmlTag(tag []byte, tagname string) bool { func isHtmlTag(tag []byte, tagname string) bool {
found, _ := findHtmlTagPos(tag, tagname)
return found
}
func findHtmlTagPos(tag []byte, tagname string) (bool, int) {
i := 0 i := 0
if i < len(tag) && tag[0] != '<' { if i < len(tag) && tag[0] != '<' {
return false return false, -1
} }
i++ i++
for i < len(tag) && isspace(tag[i]) { i = skipSpace(tag, i)
i++
}
if i < len(tag) && tag[i] == '/' { if i < len(tag) && tag[i] == '/' {
i++ i++
} }
for i < len(tag) && isspace(tag[i]) { i = skipSpace(tag, i)
i++ j := 0
}
j := i
for ; i < len(tag); i, j = i+1, j+1 { for ; i < len(tag); i, j = i+1, j+1 {
if j >= len(tagname) { if j >= len(tagname) {
break break
} }
if tag[i] != tagname[j] { if strings.ToLower(string(tag[i]))[0] != tagname[j] {
return false return false, -1
} }
} }
if i == len(tag) { if i == len(tag) {
return false return false, -1
} }
return isspace(tag[i]) || tag[i] == '>' // Now look for closing '>', but ignore it when it's in any kind of quotes,
// it might be JavaScript
inSingleQuote := false
inDoubleQuote := false
inGraveQuote := false
for i < len(tag) {
switch {
case tag[i] == '>' && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
return true, i
case tag[i] == '\'':
inSingleQuote = !inSingleQuote
case tag[i] == '"':
inDoubleQuote = !inDoubleQuote
case tag[i] == '`':
inGraveQuote = !inGraveQuote
}
i++
}
return false, -1
}
func skipSpace(tag []byte, i int) int {
for i < len(tag) && isspace(tag[i]) {
i++
}
return i
} }
func doubleSpace(out *bytes.Buffer) { func doubleSpace(out *bytes.Buffer) {

View File

@ -17,12 +17,10 @@ import (
"testing" "testing"
) )
func runMarkdownInline(input string) string { func runMarkdownInline(input string, extensions, htmlFlags int) string {
extensions := 0
extensions |= EXTENSION_AUTOLINK extensions |= EXTENSION_AUTOLINK
extensions |= EXTENSION_STRIKETHROUGH extensions |= EXTENSION_STRIKETHROUGH
htmlFlags := 0
htmlFlags |= HTML_USE_XHTML htmlFlags |= HTML_USE_XHTML
renderer := HtmlRenderer(htmlFlags, "", "") renderer := HtmlRenderer(htmlFlags, "", "")
@ -31,6 +29,10 @@ func runMarkdownInline(input string) string {
} }
func doTestsInline(t *testing.T, tests []string) { func doTestsInline(t *testing.T, tests []string) {
doTestsInlineParam(t, tests, 0, 0)
}
func doTestsInlineParam(t *testing.T, tests []string, extensions, htmlFlags int) {
// catch and report panics // catch and report panics
var candidate string var candidate string
defer func() { defer func() {
@ -43,7 +45,7 @@ func doTestsInline(t *testing.T, tests []string) {
input := tests[i] input := tests[i]
candidate = input candidate = input
expected := tests[i+1] expected := tests[i+1]
actual := runMarkdownInline(candidate) actual := runMarkdownInline(candidate, extensions, htmlFlags)
if actual != expected { if actual != expected {
t.Errorf("\nInput [%#v]\nExpected[%#v]\nActual [%#v]", t.Errorf("\nInput [%#v]\nExpected[%#v]\nActual [%#v]",
candidate, expected, actual) candidate, expected, actual)
@ -54,13 +56,48 @@ func doTestsInline(t *testing.T, tests []string) {
for start := 0; start < len(input); start++ { for start := 0; start < len(input); start++ {
for end := start + 1; end <= len(input); end++ { for end := start + 1; end <= len(input); end++ {
candidate = input[start:end] candidate = input[start:end]
_ = runMarkdownInline(candidate) _ = runMarkdownInline(candidate, extensions, htmlFlags)
} }
} }
} }
} }
} }
func TestRawHtmlTag(t *testing.T) {
tests := []string{
"zz <style>p {}</style>\n",
"<p>zz p {}</p>\n",
"zz <STYLE>p {}</STYLE>\n",
"<p>zz p {}</p>\n",
"<SCRIPT>alert()</SCRIPT>\n",
"<p>alert()</p>\n",
"zz <SCRIPT>alert()</SCRIPT>\n",
"<p>zz alert()</p>\n",
"zz <script>alert()</script>\n",
"<p>zz alert()</p>\n",
" <script>alert()</script>\n",
"<p>alert()</p>\n",
"<script>alert()</script>\n",
"<p>alert()</p>\n",
"<script src='foo'></script>\n",
"<p></p>\n",
"zz <script src='foo'></script>\n",
"<p>zz </p>\n",
"zz <script src=foo></script>\n",
"<p>zz </p>\n",
}
doTestsInlineParam(t, tests, 0, HTML_SKIP_STYLE|HTML_SKIP_SCRIPT)
}
func TestEmphasis(t *testing.T) { func TestEmphasis(t *testing.T) {
var tests = []string{ var tests = []string{
"nothing inline\n", "nothing inline\n",

View File

@ -202,7 +202,7 @@ func MarkdownBasic(input []byte) []byte {
// //
// * Smartypants processing with smart fractions and LaTeX dashes // * Smartypants processing with smart fractions and LaTeX dashes
// //
// * Intra-word emphasis supression // * Intra-word emphasis suppression
// //
// * Tables // * Tables
// //
@ -220,6 +220,7 @@ func MarkdownCommon(input []byte) []byte {
htmlFlags |= HTML_USE_SMARTYPANTS htmlFlags |= HTML_USE_SMARTYPANTS
htmlFlags |= HTML_SMARTYPANTS_FRACTIONS htmlFlags |= HTML_SMARTYPANTS_FRACTIONS
htmlFlags |= HTML_SMARTYPANTS_LATEX_DASHES htmlFlags |= HTML_SMARTYPANTS_LATEX_DASHES
htmlFlags |= HTML_SKIP_SCRIPT
renderer := HtmlRenderer(htmlFlags, "", "") renderer := HtmlRenderer(htmlFlags, "", "")
// set up the parser // set up the parser