CVE-2024-39919 (published 2024-07-15T20:15Z)

@jmondi/url-to-png is an open source URL to PNG utility featuring parallel rendering using Playwright for screenshots and with storage caching via Local, S3, or CouchDB. The package includes an `ALLOW_LIST` where the host can specify which services the user is permitted to capture screenshots of. By default, capturing screenshots of web services running on localhost, 127.0.0.1, or the [::] is allowed. If someone hosts this project on a server, users could then capture screenshots of other web services running locally. This issue has been addressed in version 2.1.1 with the addition of a blocklist. Users are advised to upgrade. There are no known workarounds for this vulnerability.


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: Nishant Jain
author_date: 2024-07-11 19:23:15-07:00
msg: block list
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: .env.sample
change_type: ModificationType.MODIFY
diff: @@ -10,6 +10,9 @@ NODE_ENV=development
 # Comma-separated list of allowed domains for screenshots (optional)
 #ALLOW_LIST=jasonraimondi.com,github.com
 
+# Comma-separated list of allowed domains for screenshots (optional)
+# BLOCK_LIST=example.com
+
 # Cache-Control header value for the responses (optional)
 #CACHE_CONTROL="public, max-age=86400, immutable"
 

programming_language: Shell
filename: app.ts
change_type: ModificationType.MODIFY
diff: @@ -8,8 +8,9 @@ import { ImageRenderInterface } from "./lib/image_render.js";
 import { logger } from "./lib/logger.js";
 import { PlainConfigSchema } from "./lib/schema.js";
 import { ImageStorage } from "./lib/storage/_base.js";
-import { formatAllowList } from "./lib/utils.js";
+import { formatUrlList } from "./lib/utils.js";
 import { handleAllowListMiddleware } from "./middlewares/allow_list.js";
+import { handleBlockListMiddleware } from "./middlewares/block_list.js";
 import { handleExtractQueryParamsMiddleware } from "./middlewares/extract_query_params.js";
 import { getIndex } from "./routes/index.js";
 
@@ -57,8 +58,14 @@ export function createApplication(
 
   app.use("/", handleExtractQueryParamsMiddleware(stringEncrypter));
 
+  if (process.env.BLOCK_LIST && process.env.BLOCK_LIST.trim() !== "") {
+    const allowList = formatUrlList(process.env.BLOCK_LIST);
+    logger.info(`Blocked Domains: ${allowList.join(", ")}`);
+    app.use("/", handleBlockListMiddleware(allowList));
+  }
+
   if (process.env.ALLOW_LIST && process.env.ALLOW_LIST.trim() !== "") {
-    const allowList = formatAllowList(process.env.ALLOW_LIST);
+    const allowList = formatUrlList(process.env.ALLOW_LIST);
     logger.info(`Allowed Domains: ${allowList.join(", ")}`);
     app.use("/", handleAllowListMiddleware(allowList));
   }

programming_language: TypeScript
filename: utils.ts
change_type: ModificationType.MODIFY
diff: @@ -1,4 +1,4 @@
-export function formatAllowList(allowList: string): string[] {
+export function formatUrlList(allowList: string): string[] {
   return allowList.split(",").map(url => {
     url = url.trim().replace(/https?:\/\//g, "");
     return new URL(`http://${url}`).host;

programming_language: TypeScript
filename: allow_list.ts
change_type: ModificationType.MODIFY
diff: @@ -7,7 +7,9 @@ import { logger } from "../lib/logger.js";
 export function handleAllowListMiddleware(allowList: string[]) {
   return async (c: Context<AppEnv>, next: () => Promise<void>) => {
     const input = c.get("input");
-    const isValidDomain = allowList.includes(new URL(input.url).host);
+    const newurl = new URL(input.url).host;
+    logger.info(`URL new: ${newurl}`);
+    const isValidDomain = allowList.includes(newurl);
 
     if (!isValidDomain) {
       logger.warn(`Blocked request to ${input.url} - not in allowlist`);

programming_language: TypeScript
filename: block_list.ts
change_type: ModificationType.ADD
diff: @@ -0,0 +1,22 @@
+// blockedHostsMiddleware.ts
+import { Context } from "hono";
+import { HTTPException } from "hono/http-exception";
+
+import { AppEnv } from "../app.js";
+import { logger } from "../lib/logger.js";
+
+export function handleBlockListMiddleware(blockList: string[]) {
+  return async (c: Context<AppEnv>, next: () => Promise<void>) => {
+    const input = c.get("input");
+    const urlHost = new URL(input.url).host;
+    logger.info(`Request URL host: ${urlHost}`);
+
+    // Check if the host is in the block list
+    if (blockList.includes(urlHost)) {
+      logger.warn(`Blocked request to ${input.url} - host is in block list`);
+      throw new HTTPException(403, { message: "Access to this URL is forbidden" });
+    }
+
+    await next();
+  };
+}

programming_language: TypeScript