fix(classes): keep raw HTML in content_html behind the safety gate
- FormatHTML renders with goldmark html.WithUnsafe(), as Winter's Markdown::parse passes raw HTML through, so an admin save, an import or an API write no longer turns imported HTML into raw HTML omitted - rejectUnsafe still runs after the render and refuses script, iframe, object and embed tags, event handlers and javascript, vbscript and data URLs; API response shapes are unchanged - TestFormatHTMLKeepsRawHTML covers HTML paragraphs and a raw footer
This commit is contained in:
@@ -9,12 +9,17 @@ import (
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
)
|
||||
|
||||
var (
|
||||
markdownEngine = goldmark.New(
|
||||
goldmark.WithExtensions(extension.Footnote, extension.Table),
|
||||
goldmark.WithParserOptions(parser.WithAttribute()),
|
||||
// Winter's Markdown::parse passes raw HTML through, so imported HTML
|
||||
// bodies round-trip unchanged (D-03, PHP parity); rejectUnsafe gates
|
||||
// the rendered result on every write path.
|
||||
goldmark.WithRendererOptions(html.WithUnsafe()),
|
||||
)
|
||||
|
||||
markdownUnsafeTag = regexp.MustCompile(`(?i)<(?:script|iframe|object|embed)\b`)
|
||||
@@ -23,35 +28,37 @@ var (
|
||||
)
|
||||
|
||||
// FormatHTML converts journal post markdown to stored HTML using goldmark
|
||||
// footnote, table, and attribute extensions, then the same rejectUnsafe gate
|
||||
// as cabana.RenderMarkdown. It does not call cabana.RenderMarkdown.
|
||||
// footnote, table, and attribute extensions. Raw HTML in the source passes
|
||||
// through unchanged; the result then goes through the same rejectUnsafe gate
|
||||
// as cabana.RenderMarkdown. It does not call cabana.RenderMarkdown, whose
|
||||
// admin preview stays without unsafe HTML.
|
||||
func FormatHTML(src string) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := markdownEngine.Convert([]byte(strings.TrimSpace(src)), &buf); err != nil {
|
||||
return "", fmt.Errorf("journal: markdown: %w", err)
|
||||
}
|
||||
html := buf.String()
|
||||
if err := rejectUnsafe(html); err != nil {
|
||||
out := buf.String()
|
||||
if err := rejectUnsafe(out); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return html, nil
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CheckHTML applies the FormatHTML safety gate to HTML that is stored as is,
|
||||
// such as category content. It refuses raw script, iframe, object and embed
|
||||
// tags, event handler attributes and javascript, vbscript or data URL schemes.
|
||||
func CheckHTML(html string) error {
|
||||
return rejectUnsafe(html)
|
||||
func CheckHTML(doc string) error {
|
||||
return rejectUnsafe(doc)
|
||||
}
|
||||
|
||||
func rejectUnsafe(html string) error {
|
||||
if markdownUnsafeTag.MatchString(html) {
|
||||
func rejectUnsafe(doc string) error {
|
||||
if markdownUnsafeTag.MatchString(doc) {
|
||||
return fmt.Errorf("journal: rendered HTML contains raw unsafe tags")
|
||||
}
|
||||
if markdownEventHandler.MatchString(html) {
|
||||
if markdownEventHandler.MatchString(doc) {
|
||||
return fmt.Errorf("journal: rendered HTML contains event handlers")
|
||||
}
|
||||
if markdownDangerousURL.MatchString(html) {
|
||||
if markdownDangerousURL.MatchString(doc) {
|
||||
return fmt.Errorf("journal: rendered HTML contains a dangerous URL scheme")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -56,6 +56,28 @@ func TestFormatHTMLRejectsUnsafeHTML(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormatHTMLKeepsRawHTML pins D-03: raw HTML in content passes through
|
||||
// unchanged, as Winter's Markdown::parse does, so an imported HTML body
|
||||
// survives an admin save, an import or an API write.
|
||||
func TestFormatHTMLKeepsRawHTML(t *testing.T) {
|
||||
const paragraphs = "<p>Grzyby rosną w lesie.</p>\n<p>Druga <b>część</b>.</p>"
|
||||
out, err := FormatHTML(paragraphs)
|
||||
if err != nil {
|
||||
t.Fatalf("html paragraphs: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(out) != paragraphs {
|
||||
t.Fatalf("html paragraphs changed:\n%s", out)
|
||||
}
|
||||
|
||||
mixed, err := FormatHTML("# Tytuł\n\nTekst.\n\n<footer class=\"author\">Autor</footer>")
|
||||
if err != nil {
|
||||
t.Fatalf("markdown with a raw footer: %v", err)
|
||||
}
|
||||
if !strings.Contains(mixed, "<h1>Tytuł</h1>") || !strings.Contains(mixed, `<footer class="author">Autor</footer>`) {
|
||||
t.Fatalf("raw footer was dropped:\n%s", mixed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestJournalEndToEnd keeps `go test ./... -run TestJournalEndToEnd`
|
||||
// from reporting [no tests to run] in this package.
|
||||
func TestJournalEndToEnd(t *testing.T) {}
|
||||
|
||||
Reference in New Issue
Block a user