Skip to content

Commit eb70161

Browse files
committed
fix(security): require screenshot protocol to be http/https
prevent file:// URI scheme in Playwright screenshots A critical vulnerability was discovered in a web application feature that utilizes Playwright's screenshot capability. Attackers could exploit this vulnerability by using the file:// URI scheme to read arbitrary files on the server's filesystem, potentially exposing sensitive information, such as AWS credentials. This commit addresses the vulnerability by implementing proper input validation and sanitization to prevent the use of the file:// URI scheme in Playwright screenshot requests, mitigating the risk of unauthorized file access. resolves #47
1 parent 57374dc commit eb70161

File tree

3 files changed

+39
-2
lines changed

3 files changed

+39
-2
lines changed

src/lib/schema.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,25 @@ const zodStringBool = z
77
.transform(x => x === "true")
88
.pipe(z.boolean());
99

10+
const urlSchema = z
11+
.string()
12+
.url()
13+
.refine(
14+
val => {
15+
try {
16+
const url = new URL(val);
17+
return url.protocol === "http:" || url.protocol === "https:";
18+
} catch (err) {
19+
return false;
20+
}
21+
},
22+
{
23+
message: "must start with http or https",
24+
},
25+
);
26+
1027
export const PlainConfigSchema = z.object({
11-
url: z.string().url(),
28+
url: urlSchema,
1229
width: z.coerce.number().nullish(),
1330
height: z.coerce.number().nullish(),
1431
viewPortWidth: z.coerce.number().nullish(),

src/middlewares/extract_query_params.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,15 @@ export function handleExtractQueryParamsMiddleware(encryptionService?: StringEnc
3333
const { validData, errors } = parseForm({ data: input, schema: PlainConfigSchema });
3434

3535
if (errors) {
36-
throw new HTTPException(400, { message: "Invalid query parameters", cause: errors });
36+
let message: string = "Invalid query parameters: ";
37+
38+
for (const [key, value] of Object.entries(errors)) {
39+
message += `\n ${key} - ${value}`;
40+
}
41+
42+
console.log(message);
43+
44+
throw new HTTPException(400, { message, cause: errors });
3745
}
3846

3947
if (validData.width && validData.width > 1920) {

tests/app.spec.ts

+12
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ suite("app", () => {
6464
expect(res.status).toBe(400);
6565
expect(await res.text()).toMatch(/Invalid query/gi);
6666
});
67+
68+
[
69+
"file:///etc/passwd&width=4000",
70+
"view-source:file:///home/&width=4000",
71+
"view-source:file:///home/ec2-user/url-to-png/.env",
72+
].forEach(invalidDomain => {
73+
it(`throws when invalid protocol ${invalidDomain}`, async () => {
74+
const res = await app.request(`/?url=${invalidDomain}`);
75+
expect(res.status).toBe(400);
76+
expect(await res.text()).toMatch(/url - must start with http or https/gi);
77+
});
78+
});
6779
});
6880

6981
describe("GET /?hash=", () => {

0 commit comments

Comments
 (0)