From 9e3f795380df50688400de91c5914507723c2d29 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 12 Mar 2025 16:52:57 +0100 Subject: [PATCH] fix: protect against stored xss in custom background url (@fehmer) (#6355) !nuf closes #6354 --- .../src/ts/controllers/theme-controller.ts | 12 ++- .../contracts/__test__/schema/config.spec.ts | 82 +++++++++++++++++++ packages/contracts/src/schemas/configs.ts | 6 +- 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 packages/contracts/__test__/schema/config.spec.ts diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index 508e2b7cd..da722c91b 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -353,9 +353,17 @@ function applyCustomBackground(): void { } else { $("#words").addClass("noErrorBorder"); $("#resultWordsHistory").addClass("noErrorBorder"); - $(".customBackground").html( - `` + + //use setAttribute for possible unsafe customBackground value + const container = document.querySelector(".customBackground"); + const img = document.createElement("img"); + img.setAttribute("src", Config.customBackground); + img.setAttribute( + "onError", + "javascript:window.dispatchEvent(new Event('customBackgroundFailed'))" ); + container?.replaceChildren(img); + BackgroundFilter.apply(); applyCustomBackgroundSize(); } diff --git a/packages/contracts/__test__/schema/config.spec.ts b/packages/contracts/__test__/schema/config.spec.ts new file mode 100644 index 000000000..6e2161513 --- /dev/null +++ b/packages/contracts/__test__/schema/config.spec.ts @@ -0,0 +1,82 @@ +import { CustomBackgroundSchema } from "../../src/schemas/configs"; + +describe("config schema", () => { + describe("CustomBackgroundSchema", () => { + it.for([ + { + name: "http", + input: `http://example.com/path/image.png`, + }, + { + name: "https", + input: `https://example.com/path/image.png`, + }, + { + name: "png", + input: `https://example.com/path/image.png`, + }, + { + name: "gif", + input: `https://example.com/path/image.gif?width=5`, + }, + { + name: "jpeg", + input: `https://example.com/path/image.jpeg`, + }, + { + name: "jpg", + input: `https://example.com/path/image.jpg`, + }, + { + name: "tiff", + input: `https://example.com/path/image.tiff`, + expectedError: "Unsupported image format.", + }, + { + name: "non-url", + input: `test`, + expectedError: "Needs to be an URI.", + }, + { + name: "single quotes", + input: `https://example.com/404.jpg?q=alert('1')`, + expectedError: "May not contain quotes.", + }, + { + name: "double quotes", + input: `https://example.com/404.jpg?q=alert("1")`, + expectedError: "May not contain quotes.", + }, + { + name: "back tick", + input: `https://example.com/404.jpg?q=alert(\`1\`)`, + expectedError: "May not contain quotes.", + }, + { + name: "javascript url", + input: `javascript:alert('asdf');//https://example.com/img.jpg`, + expectedError: "Unsupported protocol.", + }, + { + name: "data url", + input: `data:image/gif;base64,data`, + expectedError: "Unsupported protocol.", + }, + { + name: "long url", + input: `https://example.com/path/image.jpeg?q=${new Array(2048) + .fill("x") + .join()}`, + expectedError: "URL is too long.", + }, + ])(`$name`, ({ input, expectedError }) => { + const parsed = CustomBackgroundSchema.safeParse(input); + if (expectedError !== undefined) { + expect(parsed.success).toEqual(false); + expect(parsed.error?.issues[0]?.message).toEqual(expectedError); + } else { + expect(parsed.success).toEqual(true); + } + }); + }); +}); diff --git a/packages/contracts/src/schemas/configs.ts b/packages/contracts/src/schemas/configs.ts index 0b971daf0..d36e4995b 100644 --- a/packages/contracts/src/schemas/configs.ts +++ b/packages/contracts/src/schemas/configs.ts @@ -285,7 +285,11 @@ export type MaxLineWidth = z.infer; export const CustomBackgroundSchema = z .string() - .regex(/(https|http):\/\/(www\.|).+\..+\/.+(\.png|\.gif|\.jpeg|\.jpg)/gi) + .url("Needs to be an URI.") + .regex(/^(https|http):\/\/.*/, "Unsupported protocol.") + .regex(/^[^`'"]*$/, "May not contain quotes.") + .regex(/.+(\.png|\.gif|\.jpeg|\.jpg)/gi, "Unsupported image format.") + .max(2048, "URL is too long.") .or(z.literal("")); export type CustomBackground = z.infer;