CVE-2024-39919: Insufficient Information (NVD-CWE-noinfo)
value: @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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-07-15T20:15Z
Repository: https://github.com/jasonraimondi/url-to-png
Language: TypeScript
Files changed: .env.sample, src/app.ts, src/lib/utils.ts, src/middlewares/allow_list.ts, src/middlewares/block_list.ts
Commit date: 2024-07-12 20:23:06 -0400
Commit message: block list

Diff: diff --git a/.env.sample b/.env.sample
index 879fca7..1ac6790 100644
--- a/.env.sample
+++ b/.env.sample
@@ -8,10 +8,13 @@ NODE_ENV=development
 #PORT=3089
 
 # 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"
 
 # Browser timeout in milliseconds for rendering (optional)
 #BROWSER_TIMEOUT=10000
diff --git a/src/app.ts b/src/app.ts
index babc3b6..2ac5c6b 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -6,12 +6,13 @@ import { StatusCode } from "hono/utils/http-status";
 import { BrowserPool } from "./lib/browser_pool.js";
 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";
 
 export type Variables = {
   input: PlainConfigSchema;
@@ -55,12 +56,18 @@ export function createApplication(
     return c.json({ message: err.message }, status);
   });
 
   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));
   }
 
   app.get("/", getIndex(imageStorageService, imageRenderService));
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 86b38a9..c4062f0 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,6 @@
-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;
   });
 }
diff --git a/src/middlewares/allow_list.ts b/src/middlewares/allow_list.ts
index 04891b7..45021c0 100644
--- a/src/middlewares/allow_list.ts
+++ b/src/middlewares/allow_list.ts
@@ -5,11 +5,13 @@ import { AppEnv } from "../app.js";
 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`);
       throw new HTTPException(400, { message: "Invalid URL Requested" });
     }
diff --git a/src/middlewares/block_list.ts b/src/middlewares/block_list.ts
new file mode 100644
index 0000000..30888e1
--- /dev/null
+++ b/src/middlewares/block_list.ts
@@ -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();
+  };
+}

Vulnerable code: import { formatAllowList } from "./lib/utils.js";
    const allowList = formatAllowList(process.env.ALLOW_LIST);
export function formatAllowList(allowList: string): string[] {
    const isValidDomain = allowList.includes(new URL(input.url).host);
Fixed code: # Comma-separated list of allowed domains for screenshots (optional)
# BLOCK_LIST=example.com

import { formatUrlList } from "./lib/utils.js";
import { handleBlockListMiddleware } from "./middlewares/block_list.js";
  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));
  }

    const allowList = formatUrlList(process.env.ALLOW_LIST);
export function formatUrlList(allowList: string): string[] {
    const newurl = new URL(input.url).host;
    logger.info(`URL new: ${newurl}`);
    const isValidDomain = allowList.includes(newurl);
// 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();
  };
}