Escape single quotes on the buyer-facing buy page

buy_page.rs kept a private html_escape that omitted the `'` escape the
canonical api::mod.rs impl has, so operator/product/discount-code text
rendered into HTML attributes was under-escaped. Drop the fork, reuse the
canonical escaper, and add a unit test covering the single quote.
This commit is contained in:
Grant
2026-06-19 23:15:09 -05:00
parent fd71b19f86
commit 1faab61098
2 changed files with 22 additions and 7 deletions
+3 -7
View File
@@ -16,6 +16,9 @@
//! buyer-facing surface — easy to deploy, no asset hosting required.
use crate::api::AppState;
// Reuse the canonical HTML escaper (escapes `'` as well as `&<>"`) instead of a
// private copy, so the buyer-facing page can't fall behind on attribute escaping.
use crate::api::html_escape;
use crate::db::repo;
use axum::{
extract::{Path, Query, State},
@@ -1533,13 +1536,6 @@ code{{background:#eee;padding:0.1em 0.4em;border-radius:4px;font-family:ui-monos
)
}
fn html_escape(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
}
fn format_thousands(n: i64) -> String {
// Renders 50000 as "50,000" — visible price legibility for sat amounts.
let s = n.to_string();
+19
View File
@@ -1193,3 +1193,22 @@ async fn pubkey(
"public_key_pem": state.keypair.public_key_pem,
}))
}
#[cfg(test)]
mod tests {
use super::*;
/// The canonical escaper must cover the single quote — operator/product/
/// discount-code text renders into HTML attributes (incl. single-quoted),
/// so omitting `'` is an injection hole. Guards against re-forking a copy
/// that drops it (the bug that lived in `buy_page.rs`).
#[test]
fn html_escape_covers_single_quote_and_friends() {
assert_eq!(html_escape("'"), "&#39;");
assert_eq!(
html_escape(r#"<a href='x' title="y">&</a>"#),
"&lt;a href=&#39;x&#39; title=&quot;y&quot;&gt;&amp;&lt;/a&gt;"
);
assert_eq!(html_escape("plain"), "plain");
}
}