Fix XSS in cookie banner translation rendering

Validate translation string values server-side with NoHTML() and
MaxLen(2000) to reject HTML in the translations JSON blob. On the
client side, escape user-provided template text before innerHTML
injection in banner_description and placeholder_text paths.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-23 20:29:39 +04:00
parent cd824c2c55
commit e24813202b
3 changed files with 41 additions and 5 deletions

View File

@@ -13,7 +13,6 @@
// PERFORMANCE OF THIS SOFTWARE.
import type { BannerTexts } from "./i18n";
import { interpolate } from "./i18n";
import { removeCookies } from "./cookie-utils";
import { LOCK_ICON } from "./html";
@@ -155,9 +154,13 @@ function createPlaceholder(
}
}
const phText = texts?.placeholder_text
? interpolate(texts.placeholder_text, { category: escapeHtml(displayLabel) })
: `This content requires <strong>${escapeHtml(displayLabel)}</strong> cookies.`;
let phText: string;
if (texts?.placeholder_text) {
const parts = texts.placeholder_text.split("{{category}}");
phText = parts.map(p => escapeHtml(p)).join(`<strong>${escapeHtml(displayLabel)}</strong>`);
} else {
phText = `This content requires <strong>${escapeHtml(displayLabel)}</strong> cookies.`;
}
const phButton = texts?.placeholder_button ?? "Manage cookie preferences";
placeholder.innerHTML = [

View File

@@ -179,7 +179,8 @@ export class ProboThemedBanner extends HTMLElement {
const linkText = this.esc(texts.privacy_policy_link_text ?? "Privacy Policy");
link = `<a href="${this.esc(config.privacy_policy_url)}" target="_blank" rel="noopener noreferrer">${linkText}</a>`;
}
el.innerHTML = interpolate(raw, { privacy_policy_link: link });
const parts = raw.split("{{privacy_policy_link}}");
el.innerHTML = parts.map(p => this.esc(p)).join(link);
} else if (key === "panel_description") {
el.textContent = interpolate(raw, { necessary_category: necessaryCategoryName });
} else {