Skip to content

Commit 9e3f795

Browse files
authored
fix: protect against stored xss in custom background url (@fehmer) (monkeytypegame#6355)
!nuf closes monkeytypegame#6354
1 parent 220f2b8 commit 9e3f795

File tree

3 files changed

+97
-3
lines changed

3 files changed

+97
-3
lines changed

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,17 @@ function applyCustomBackground(): void {
353353
} else {
354354
$("#words").addClass("noErrorBorder");
355355
$("#resultWordsHistory").addClass("noErrorBorder");
356-
$(".customBackground").html(
357-
`<img src="${Config.customBackground}" 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'))"
358364
);
365+
container?.replaceChildren(img);
366+
359367
BackgroundFilter.apply();
360368
applyCustomBackgroundSize();
361369
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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: "single quotes",
42+
input: `https://example.com/404.jpg?q=alert('1')`,
43+
expectedError: "May not contain quotes.",
44+
},
45+
{
46+
name: "double quotes",
47+
input: `https://example.com/404.jpg?q=alert("1")`,
48+
expectedError: "May not contain quotes.",
49+
},
50+
{
51+
name: "back tick",
52+
input: `https://example.com/404.jpg?q=alert(\`1\`)`,
53+
expectedError: "May not contain quotes.",
54+
},
55+
{
56+
name: "javascript url",
57+
input: `javascript:alert('asdf');//https://example.com/img.jpg`,
58+
expectedError: "Unsupported protocol.",
59+
},
60+
{
61+
name: "data url",
62+
input: `data:image/gif;base64,data`,
63+
expectedError: "Unsupported protocol.",
64+
},
65+
{
66+
name: "long url",
67+
input: `https://example.com/path/image.jpeg?q=${new Array(2048)
68+
.fill("x")
69+
.join()}`,
70+
expectedError: "URL is too long.",
71+
},
72+
])(`$name`, ({ input, expectedError }) => {
73+
const parsed = CustomBackgroundSchema.safeParse(input);
74+
if (expectedError !== undefined) {
75+
expect(parsed.success).toEqual(false);
76+
expect(parsed.error?.issues[0]?.message).toEqual(expectedError);
77+
} else {
78+
expect(parsed.success).toEqual(true);
79+
}
80+
});
81+
});
82+
});

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)