Skip to content

Commit 4a11cb4

Browse files
committed
improve background url validation
1 parent b2592fc commit 4a11cb4

File tree

3 files changed

+87
-5
lines changed

3 files changed

+87
-5
lines changed

frontend/src/ts/controllers/theme-controller.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,11 +353,17 @@ function applyCustomBackground(): void {
353353
} else {
354354
$("#words").addClass("noErrorBorder");
355355
$("#resultWordsHistory").addClass("noErrorBorder");
356-
$(".customBackground").html(
357-
`<img src="${encodeURI(
358-
Config.customBackground
359-
)}" alt="" onerror="javascript:window.dispatchEvent(new Event('customBackgroundFailed'))" />`
356+
357+
//use setAttribute for possible unsafe customBackground value
358+
const container = document.querySelector(".customBackground");
359+
const img = document.createElement("img");
360+
img.setAttribute("src", Config.customBackground);
361+
img.setAttribute(
362+
"onError",
363+
"javascript:window.dispatchEvent(new Event('customBackgroundFailed'))"
360364
);
365+
container?.replaceChildren(img);
366+
361367
BackgroundFilter.apply();
362368
applyCustomBackgroundSize();
363369
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { CustomBackgroundSchema } from "../../src/schemas/configs";
2+
3+
describe("config schema", () => {
4+
describe("CustomBackgroundSchema", () => {
5+
it.for([
6+
{
7+
name: "http",
8+
input: `http://example.com/path/image.png`,
9+
},
10+
{
11+
name: "https",
12+
input: `https://example.com/path/image.png`,
13+
},
14+
{
15+
name: "png",
16+
input: `https://example.com/path/image.png`,
17+
},
18+
{
19+
name: "gif",
20+
input: `https://example.com/path/image.gif?width=5`,
21+
},
22+
{
23+
name: "jpeg",
24+
input: `https://example.com/path/image.jpeg`,
25+
},
26+
{
27+
name: "jpg",
28+
input: `https://example.com/path/image.jpg`,
29+
},
30+
{
31+
name: "tiff",
32+
input: `https://example.com/path/image.tiff`,
33+
expectedError: "Unsupported image format.",
34+
},
35+
{
36+
name: "non-url",
37+
input: `test`,
38+
expectedError: "Needs to be an URI.",
39+
},
40+
{
41+
name: "double quotes",
42+
input: `https://example.com/404.jpg?q="onerror="alert(\`1\`)"`,
43+
expectedError: "May not contain quotes.",
44+
},
45+
{
46+
name: "javascript url",
47+
input: `javascript://alert('asdf')//https://example.com/img.jpg`,
48+
expectedError: "Unsupported protocol.",
49+
},
50+
{
51+
name: "data url",
52+
input: `data:image/gif;base64,data`,
53+
expectedError: "Unsupported protocol.",
54+
},
55+
{
56+
name: "long url",
57+
input: `https://example.com/path/image.jpeg?q=${new Array(2048)
58+
.fill("x")
59+
.join()}`,
60+
expectedError: "URL is too long.",
61+
},
62+
])(`$name`, ({ input, expectedError }) => {
63+
const parsed = CustomBackgroundSchema.safeParse(input);
64+
if (expectedError !== undefined) {
65+
expect(parsed.success).toEqual(false);
66+
expect(parsed.error?.issues[0]?.message).toEqual(expectedError);
67+
} else {
68+
expect(parsed.success).toEqual(true);
69+
}
70+
});
71+
});
72+
});

packages/contracts/src/schemas/configs.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,11 @@ export type MaxLineWidth = z.infer<typeof MaxLineWidthSchema>;
285285

286286
export const CustomBackgroundSchema = z
287287
.string()
288-
.regex(/(https|http):\/\/(www\.|).+\..+\/.+(\.png|\.gif|\.jpeg|\.jpg)/gi)
288+
.url("Needs to be an URI.")
289+
.regex(/^(https|http):\/\/.*/, "Unsupported protocol.")
290+
.regex(/^[^'"]*$/, "May not contain quotes.")
291+
.regex(/.+(\.png|\.gif|\.jpeg|\.jpg)/gi, "Unsupported image format.")
292+
.max(2048, "URL is too long.")
289293
.or(z.literal(""));
290294
export type CustomBackground = z.infer<typeof CustomBackgroundSchema>;
291295

0 commit comments

Comments
 (0)