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:
@@ -13,7 +13,6 @@
|
|||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import type { BannerTexts } from "./i18n";
|
import type { BannerTexts } from "./i18n";
|
||||||
import { interpolate } from "./i18n";
|
|
||||||
import { removeCookies } from "./cookie-utils";
|
import { removeCookies } from "./cookie-utils";
|
||||||
import { LOCK_ICON } from "./html";
|
import { LOCK_ICON } from "./html";
|
||||||
|
|
||||||
@@ -155,9 +154,13 @@ function createPlaceholder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const phText = texts?.placeholder_text
|
let phText: string;
|
||||||
? interpolate(texts.placeholder_text, { category: escapeHtml(displayLabel) })
|
if (texts?.placeholder_text) {
|
||||||
: `This content requires <strong>${escapeHtml(displayLabel)}</strong> cookies.`;
|
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";
|
const phButton = texts?.placeholder_button ?? "Manage cookie preferences";
|
||||||
|
|
||||||
placeholder.innerHTML = [
|
placeholder.innerHTML = [
|
||||||
|
|||||||
@@ -179,7 +179,8 @@ export class ProboThemedBanner extends HTMLElement {
|
|||||||
const linkText = this.esc(texts.privacy_policy_link_text ?? "Privacy Policy");
|
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>`;
|
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") {
|
} else if (key === "panel_description") {
|
||||||
el.textContent = interpolate(raw, { necessary_category: necessaryCategoryName });
|
el.textContent = interpolate(raw, { necessary_category: necessaryCategoryName });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -276,6 +276,38 @@ func (r *UpsertCookieBannerTranslationRequest) Validate() error {
|
|||||||
v.Check(r.CookieBannerID, "cookie_banner_id", validator.Required(), validator.GID(coredata.CookieBannerEntityType))
|
v.Check(r.CookieBannerID, "cookie_banner_id", validator.Required(), validator.GID(coredata.CookieBannerEntityType))
|
||||||
v.Check(r.Language, "language", validator.Required(), validator.SafeTextNoNewLine(10))
|
v.Check(r.Language, "language", validator.Required(), validator.SafeTextNoNewLine(10))
|
||||||
|
|
||||||
|
var flat map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(r.Translations, &flat); err != nil {
|
||||||
|
v.Check("", "translations", validator.Required())
|
||||||
|
return v.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, raw := range flat {
|
||||||
|
if key == "categories" {
|
||||||
|
var cats map[string]json.RawMessage
|
||||||
|
if json.Unmarshal(raw, &cats) == nil {
|
||||||
|
for catID, catRaw := range cats {
|
||||||
|
var catFields map[string]json.RawMessage
|
||||||
|
if json.Unmarshal(catRaw, &catFields) == nil {
|
||||||
|
for field, fieldRaw := range catFields {
|
||||||
|
var s string
|
||||||
|
if json.Unmarshal(fieldRaw, &s) == nil {
|
||||||
|
v.Check(s, fmt.Sprintf("translations.categories.%s.%s", catID, field), validator.NoHTML(), validator.MaxLen(2000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var s string
|
||||||
|
if json.Unmarshal(raw, &s) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v.Check(s, "translations."+key, validator.NoHTML(), validator.MaxLen(2000))
|
||||||
|
}
|
||||||
|
|
||||||
return v.Error()
|
return v.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user