release: v1.16.0 — offer live pricing, bot health telemetry, edge blocklist #247
@@ -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)
|
||||
}
|
||||
|
||||
@@ -60,6 +60,30 @@ func TestProjectOfferPricingMissingRailAndEscaping(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestProjectOfferPricingEscapesHTMLAndLinks checks an admin title carrying HTML or a markdown link
|
||||
// is neutralised so it cannot inject markup into the public offer: the tag becomes entities and the
|
||||
// link brackets are escaped (so no "javascript:" anchor forms). The raw metacharacters must not
|
||||
// survive into the projected markdown.
|
||||
func TestProjectOfferPricingEscapesHTMLAndLinks(t *testing.T) {
|
||||
entries := []catalogEntry{{
|
||||
id: uuid.New(),
|
||||
title: `<script>alert(1)</script> [x](javascript:alert(2)) & "q"`,
|
||||
atoms: []atomQty{{atomType: "hints", quantity: 1}},
|
||||
prices: []priceRow{{method: "", currency: CurrencyChip, amount: 5}},
|
||||
}}
|
||||
md := projectOfferPricing(entries)
|
||||
for _, bad := range []string{"<script>", "</script>", "[x]", `& "q"`} {
|
||||
if strings.Contains(md, bad) {
|
||||
t.Errorf("unescaped %q survived into the projection:\n%s", bad, md)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{"<script>", `\[x\]`, "&", ""q""} {
|
||||
if !strings.Contains(md, want) {
|
||||
t.Errorf("want escaped %q in:\n%s", want, md)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestProjectOfferPricingEmpty checks an empty catalog projects to the empty string (no stray table
|
||||
// headers), so the offer's pricing marker is replaced with nothing.
|
||||
func TestProjectOfferPricingEmpty(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user