fix(clib): escape markdown fallback (#1290)
FromSi
created 1 month ago
## What?
Adds a safe HTML fallback for Markdown conversion failures. When
`MarkdownToHTML` fails in either the cgo `md4c` path or the no-cgo
`goldmark` path, it now logs the failure and returns escaped plain text
wrapped in `<pre>` instead of returning raw Markdown bytes.
Also adds a focused test for the fallback escaping behavior.
## Why?
Closes #571
Previously, failed Markdown conversion returned the original Markdown
bytes. The email viewer then tried to parse those raw bytes as HTML,
which could render poorly and treat HTML-looking plain text as markup.
The new fallback keeps failed conversions readable in the HTML viewer
while escaping unsafe HTML-like input.
Change summary
clib/markdown.go | 8 ++++++--
clib/markdown_fallback.go | 7 +++++++
clib/markdown_fallback_test.go | 25 +++++++++++++++++++++++++
clib/markdown_nocgo.go | 4 +++-
4 files changed, 41 insertions(+), 3 deletions(-)
Detailed changes
@@ -56,7 +56,10 @@ static char* md4c_to_html(const char* input, size_t input_len, size_t* out_len)
}
*/
import "C"
-import "unsafe"
+import (
+ "log"
+ "unsafe"
+)
// MarkdownToHTML converts Markdown bytes to HTML using md4c (C).
// This is significantly faster than goldmark for large documents.
@@ -71,7 +74,8 @@ func MarkdownToHTML(md []byte) []byte {
var outLen C.size_t
result := C.md4c_to_html((*C.char)(cInput), C.size_t(len(md)), &outLen)
if result == nil {
- return md // fallback to original on failure
+ log.Printf("markdown: md4c_to_html failed, falling back to escaped plain-text HTML")
+ return markdownPlainTextHTML(md)
}
defer C.free(unsafe.Pointer(result))
@@ -0,0 +1,7 @@
+package clib
+
+import "html"
+
+func markdownPlainTextHTML(md []byte) []byte {
+ return []byte("<pre>" + html.EscapeString(string(md)) + "</pre>")
+}
@@ -0,0 +1,25 @@
+package clib
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestMarkdownPlainTextHTMLFallbackEscapesInput(t *testing.T) {
+ input := []byte("# Heading\n\n<script>alert(\"x\")</script>\n<b>literal</b>")
+
+ got := string(markdownPlainTextHTML(input))
+
+ if !strings.HasPrefix(got, "<pre>") || !strings.HasSuffix(got, "</pre>") {
+ t.Fatalf("fallback should wrap content in pre tag, got %q", got)
+ }
+ if strings.Contains(got, "<script>") || strings.Contains(got, "<b>") {
+ t.Fatalf("fallback should escape HTML-looking input, got %q", got)
+ }
+ if !strings.Contains(got, "<script>alert("x")</script>") {
+ t.Fatalf("fallback missing escaped script text, got %q", got)
+ }
+ if !strings.Contains(got, "# Heading") {
+ t.Fatalf("fallback should preserve markdown text, got %q", got)
+ }
+}
@@ -4,6 +4,7 @@ package clib
import (
"bytes"
+ "log"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
@@ -18,7 +19,8 @@ func MarkdownToHTML(md []byte) []byte {
),
)
if err := p.Convert(md, &buf); err != nil {
- return md
+ log.Printf("markdown: goldmark conversion failed, falling back to escaped plain-text HTML: %v", err)
+ return markdownPlainTextHTML(md)
}
return buf.Bytes()
}