fix(api): sanitize translated content_html on every write
A translated content_html sent through the Journal API (syncTranslations
formatted only when content_html was empty) or through
journal:import-posts (no check) was stored verbatim and served by the
anonymous GET /_journal/api/v1/posts and /posts/{slug}. A translation
keyed on the default locale also wrote the host content_html column
past FormatHTML.
classes.FormatTranslation now sets content_html for every translated
locale before it is stored: rendered from a non-blank content with
FormatHTML (host-column parity; a supplied content_html is replaced),
otherwise a supplied content_html is sanitized with the new
classes.SanitizeHTML, which FormatHTML reuses. Both write paths call it.
PHP parity note: Winter's PostApiController::syncTranslations called
Post::formatHtml only when content_html was empty and otherwise stored
the supplied content_html verbatim; ImportPosts::syncTranslations stored
it verbatim; the MLJournalMarkdown admin widget always derived it from
content via formatHtml. The Go port now follows the admin widget on
every path. Response shapes and status codes for valid input are
unchanged; only the stored content_html of unsafe or content-mismatched
translations differs.
This commit is contained in:
@@ -72,8 +72,54 @@ func FormatHTML(src string) (string, error) {
|
||||
if err := markdownEngine.Convert([]byte(strings.TrimSpace(src)), &buf); err != nil {
|
||||
return "", fmt.Errorf("journal: markdown: %w", err)
|
||||
}
|
||||
out, _ := sanitizeHTML(buf.String())
|
||||
return out, nil
|
||||
return SanitizeHTML(buf.String()), nil
|
||||
}
|
||||
|
||||
// SanitizeHTML filters doc, which is HTML and not markdown, through the
|
||||
// FormatHTML allow-list and strips what the policy does not allow. HTML the
|
||||
// policy keeps whole is returned byte for byte. It is the gate for
|
||||
// content_html supplied without markdown, such as a translation that carries
|
||||
// only content_html.
|
||||
func SanitizeHTML(doc string) string {
|
||||
out, _ := sanitizeHTML(doc)
|
||||
return out
|
||||
}
|
||||
|
||||
// FormatTranslation sets attrs["content_html"] for one locale's translated
|
||||
// post attributes, so a translated content_html is stored under the same
|
||||
// policy as the host column. When attrs["content"] is not blank, content_html
|
||||
// is rendered from it with FormatHTML and a supplied content_html is
|
||||
// replaced, as the host column is always derived from content. Otherwise a
|
||||
// supplied, non-null content_html is sanitized with SanitizeHTML; a value
|
||||
// that is not a string is sanitized as its text form. attrs without content
|
||||
// or content_html is left unchanged.
|
||||
func FormatTranslation(attrs map[string]any) error {
|
||||
if attrs == nil {
|
||||
return nil
|
||||
}
|
||||
if content := textOf(attrs["content"]); strings.TrimSpace(content) != "" {
|
||||
html, err := FormatHTML(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attrs["content_html"] = html
|
||||
return nil
|
||||
}
|
||||
if v, has := attrs["content_html"]; has && v != nil {
|
||||
attrs["content_html"] = SanitizeHTML(textOf(v))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func textOf(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return t
|
||||
default:
|
||||
return fmt.Sprint(t)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckHTML gates HTML that is stored as is, such as category content. It
|
||||
|
||||
Reference in New Issue
Block a user