CVE-2024-37169 (published 2024-06-10T22:15Z)

@jmondi/url-to-png is a self-hosted URL to PNG utility. Versions prior to 2.0.3 are vulnerable to arbitrary file read if a threat actor uses the Playright's screenshot feature to exploit the file wrapper. Version 2.0.3 mitigates this issue by requiring input URLs to be of protocol `http` or `https`. No known workarounds are available aside from upgrading.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/jasonraimondi/url-to-png
commit_details: repo_url: https://github.com/jasonraimondi/url-to-png
author: Jason Raimondi
author_date: 2024-06-03 21:59:20-04:00
msg: 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
repository: repo_url: https://github.com/jasonraimondi/url-to-png
repo_name: jasonraimondi/url-to-png
description: Selfhosted. URL to PNG utility featuring parallel rendering using Playwright for screenshots and with storage caching via Local, S3, or CouchDB
repo_language: TypeScript
owner: jasonraimondi
file_changes: filename: schema.ts
change_type: ModificationType.MODIFY
diff: @@ -7,10 +7,25 @@ const zodStringBool = z
   .transform(x => x === "true")
   .pipe(z.boolean());
 
-const zodStringUrl = z.string().url();
+const urlSchema = z
+  .string()
+  .url()
+  .refine(
+    val => {
+      try {
+        const url = new URL(val);
+        return url.protocol === "http:" || url.protocol === "https:";
+      } catch (err) {
+        return false;
+      }
+    },
+    {
+      message: "must start with http or https",
+    },
+  );
 
 export const PlainConfigSchema = z.object({
-  url: zodStringUrl,
+  url: urlSchema,
   width: z.coerce.number().nullish(),
   height: z.coerce.number().nullish(),
   viewPortWidth: z.coerce.number().nullish(),

programming_language: TypeScript
filename: extract_query_params.ts
change_type: ModificationType.MODIFY
diff: @@ -33,7 +33,15 @@ export function handleExtractQueryParamsMiddleware(encryptionService?: StringEnc
     const { validData, errors } = parseForm({ data: input, schema: PlainConfigSchema });
 
     if (errors) {
-      throw new HTTPException(400, { message: "Invalid query parameters", cause: errors });
+      let message: string = "Invalid query parameters: ";
+
+      const specificErrors = Object.entries(errors).map(([key, value]) => `(${key} - ${value})`).join(" ")
+
+      message = `${message} ${specificErrors}`;
+
+      console.log(message);
+
+      throw new HTTPException(400, { message, cause: errors });
     }
 
     if (validData.width && validData.width > 1920) {

programming_language: TypeScript
filename: app.spec.ts
change_type: ModificationType.MODIFY
diff: @@ -71,6 +71,18 @@ suite("app", () => {
       expect(res.status).toBe(400);
       expect(await res.text()).toMatch(/Invalid query/gi);
     });
+
+    [
+      "file:///etc/passwd&width=4000",
+      "view-source:file:///home/&width=4000",
+      "view-source:file:///home/ec2-user/url-to-png/.env",
+    ].forEach(invalidDomain => {
+      it(`throws when invalid protocol ${invalidDomain}`, async () => {
+        const res = await app.request(`/?url=${invalidDomain}`);
+        expect(res.status).toBe(400);
+        expect(await res.text()).toMatch(/url - must start with http or https/gi);
+      });
+    });
   });
 
   describe("GET /?hash=", () => {

programming_language: TypeScript