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;