diff --git a/licensing-service/src/api/buy_page.rs b/licensing-service/src/api/buy_page.rs index 94b3904..09961cf 100644 --- a/licensing-service/src/api/buy_page.rs +++ b/licensing-service/src/api/buy_page.rs @@ -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('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) -} - fn format_thousands(n: i64) -> String { // Renders 50000 as "50,000" — visible price legibility for sat amounts. let s = n.to_string(); diff --git a/licensing-service/src/api/mod.rs b/licensing-service/src/api/mod.rs index a56197e..fe4246b 100644 --- a/licensing-service/src/api/mod.rs +++ b/licensing-service/src/api/mod.rs @@ -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("'"), "'"); + assert_eq!( + html_escape(r#"&"#), + "<a href='x' title="y">&</a>" + ); + assert_eq!(html_escape("plain"), "plain"); + } +}