harden(offer): escape admin product titles against HTML/markdown injection
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m21s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m21s
The offer price list projects the admin-entered product title into a markdown table cell that is rendered, unsanitised, into the public /offer/ page. Escape the title at the projection boundary so it renders as literal text: HTML metacharacters become entities and the markdown table pipe and link brackets are escaped, so a title can neither inject markup nor form a javascript: link. The committed offer prose keeps its trusted-content treatment (code-reviewed, not runtime input).
This commit is contained in:
@@ -117,9 +117,28 @@ func offerPrice(e catalogEntry, method string, cur Currency) string {
|
||||
return "—"
|
||||
}
|
||||
|
||||
// offerCell escapes an owner-entered title for a markdown table cell: a literal pipe would break the
|
||||
// column layout, and a newline would break the row.
|
||||
// offerCellReplacer neutralises every metacharacter of an admin-entered title so it renders as
|
||||
// literal text in the public offer. The title is operator input (the /_gm catalog editor) that flows
|
||||
// into a markdown table cell and then through marked into the /offer/ HTML, which is deliberately not
|
||||
// sanitised — so escaping here is the trust boundary. It covers HTML (no tag or entity reaches the
|
||||
// page), the markdown table pipe and the row newline, and the link brackets (a title must never
|
||||
// become a "javascript:" link). marked passes the entities through unchanged, so the reader sees the
|
||||
// exact title. NewReplacer scans once and never re-scans its own output, so "&" → "&" does not
|
||||
// double-escape the entities the other rules emit.
|
||||
var offerCellReplacer = strings.NewReplacer(
|
||||
"&", "&",
|
||||
"<", "<",
|
||||
">", ">",
|
||||
`"`, """,
|
||||
"'", "'",
|
||||
"|", `\|`,
|
||||
"[", `\[`,
|
||||
"]", `\]`,
|
||||
"\n", " ",
|
||||
)
|
||||
|
||||
// offerCell escapes an admin-entered title for safe, literal rendering in a markdown table cell of
|
||||
// the public offer (see [offerCellReplacer]).
|
||||
func offerCell(s string) string {
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
return strings.ReplaceAll(s, "|", "\\|")
|
||||
return offerCellReplacer.Replace(s)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user