mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-13 05:16:29 +02:00
fix(sanitize): preserve code blocks and inline code from HTML entity escaping
Bluemonday operates on raw text, so characters like && and <> inside markdown code blocks/inline code were being HTML-escaped (e.g. && → &&), causing them to render incorrectly in the frontend. Now extracts fenced code blocks and inline code spans before sanitization, runs bluemonday on the remaining content, then restores the code verbatim.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package sanitize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
@@ -11,11 +13,6 @@ var httpURL = regexp.MustCompile(`^https?://`)
|
||||
|
||||
// policy is a shared bluemonday policy that allows safe Markdown HTML while
|
||||
// stripping dangerous elements (script, iframe, object, embed, style, on*).
|
||||
//
|
||||
// Note: bluemonday operates on raw text, so HTML inside Markdown code blocks
|
||||
// (e.g. ```<script>```) will also be stripped. This is an acceptable trade-off
|
||||
// for defense-in-depth — the primary sanitization happens in the frontend via
|
||||
// rehype-sanitize which understands the Markdown AST.
|
||||
var policy *bluemonday.Policy
|
||||
|
||||
func init() {
|
||||
@@ -28,8 +25,47 @@ func init() {
|
||||
policy.AllowAttrs("class").OnElements("code", "div", "span", "pre")
|
||||
}
|
||||
|
||||
// fencedCodeBlock matches ``` or ~~~ fenced code blocks (with optional language tag).
|
||||
var fencedCodeBlock = regexp.MustCompile("(?m)^(```|~~~)[^\n]*\n[\\s\\S]*?\n(```|~~~)[ \t]*$")
|
||||
|
||||
// inlineCode matches backtick-delimited inline code spans.
|
||||
// Ordered longest-delimiter-first so triple backticks match before doubles/singles.
|
||||
var inlineCode = regexp.MustCompile("```[^`]+```|``[^`]+``|`[^`]+`")
|
||||
|
||||
// HTML sanitizes user-provided HTML/Markdown content, stripping dangerous
|
||||
// tags (script, iframe, object, embed, etc.) and event-handler attributes.
|
||||
//
|
||||
// Code blocks and inline code spans are preserved verbatim so that bluemonday
|
||||
// does not HTML-escape their contents (e.g. && → &&).
|
||||
func HTML(input string) string {
|
||||
return policy.Sanitize(input)
|
||||
// 1. Extract fenced code blocks, replacing with unique placeholders.
|
||||
var blocks []string
|
||||
placeholder := func(i int) string { return fmt.Sprintf("\x00CODEBLOCK_%d\x00", i) }
|
||||
result := fencedCodeBlock.ReplaceAllStringFunc(input, func(m string) string {
|
||||
idx := len(blocks)
|
||||
blocks = append(blocks, m)
|
||||
return placeholder(idx)
|
||||
})
|
||||
|
||||
// 2. Extract inline code spans.
|
||||
var inlines []string
|
||||
inlinePH := func(i int) string { return fmt.Sprintf("\x00INLINE_%d\x00", i) }
|
||||
result = inlineCode.ReplaceAllStringFunc(result, func(m string) string {
|
||||
idx := len(inlines)
|
||||
inlines = append(inlines, m)
|
||||
return inlinePH(idx)
|
||||
})
|
||||
|
||||
// 3. Sanitize the non-code portions.
|
||||
result = policy.Sanitize(result)
|
||||
|
||||
// 4. Restore inline code spans, then fenced code blocks.
|
||||
for i, code := range inlines {
|
||||
result = strings.Replace(result, inlinePH(i), code, 1)
|
||||
}
|
||||
for i, block := range blocks {
|
||||
result = strings.Replace(result, placeholder(i), block, 1)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -80,6 +80,52 @@ func TestHTML(t *testing.T) {
|
||||
input: `<div data-type="fileCard" data-href="http://example.com/file.pdf" data-filename="file.pdf"></div>`,
|
||||
want: `<div data-type="fileCard" data-href="http://example.com/file.pdf" data-filename="file.pdf"></div>`,
|
||||
},
|
||||
// Code block preservation — entities must NOT be escaped inside code.
|
||||
{
|
||||
name: "fenced code block preserves ampersands",
|
||||
input: "```\na && b\n```",
|
||||
want: "```\na && b\n```",
|
||||
},
|
||||
{
|
||||
name: "fenced code block preserves angle brackets",
|
||||
input: "```html\n<div class=\"x\">hello</div>\n```",
|
||||
want: "```html\n<div class=\"x\">hello</div>\n```",
|
||||
},
|
||||
{
|
||||
name: "inline code preserves ampersands",
|
||||
input: "run `a && b` in shell",
|
||||
want: "run `a && b` in shell",
|
||||
},
|
||||
{
|
||||
name: "inline code preserves angle brackets",
|
||||
input: "use `x < y && y > z`",
|
||||
want: "use `x < y && y > z`",
|
||||
},
|
||||
{
|
||||
name: "double backtick inline code preserved",
|
||||
input: "use ``a && b`` here",
|
||||
want: "use ``a && b`` here",
|
||||
},
|
||||
{
|
||||
name: "script in fenced code block preserved",
|
||||
input: "```\n<script>alert(1)</script>\n```",
|
||||
want: "```\n<script>alert(1)</script>\n```",
|
||||
},
|
||||
{
|
||||
name: "script outside code block still stripped",
|
||||
input: "hello <script>alert(1)</script> world",
|
||||
want: "hello world",
|
||||
},
|
||||
{
|
||||
name: "mixed code and non-code",
|
||||
input: "text `a && b` more <script>x</script> end",
|
||||
want: "text `a && b` more end",
|
||||
},
|
||||
{
|
||||
name: "tilde fenced code block preserves content",
|
||||
input: "~~~\na && b\n~~~",
|
||||
want: "~~~\na && b\n~~~",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user