CVE-2021-4437: Inefficient Regular Expression Complexity (CWE-1333)
value: A vulnerability, which was classified as problematic, has been found in dbartholomae lambda-middleware frameguard up to 1.0.4. Affected by this issue is some unknown functionality of the file packages/json-deserializer/src/JsonDeserializer.ts of the component JSON Mime-Type Handler. The manipulation leads to inefficient regular expression complexity. Upgrading to version 1.1.0 is able to address this issue. The patch is identified as f689404d830cbc1edd6a1018d3334ff5f44dc6a6. It is recommended to upgrade the affected component. VDB-253406 is the identifier assigned to this vulnerability.
CWE description: The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles.
Published: 2024-02-12T20:15Z
Repository: https://github.com/dbartholomae/lambda-middleware
Language: TypeScript
Files changed: packages/json-deserializer/src/JsonDeserializer.test.ts, packages/json-deserializer/src/JsonDeserializer.ts
Commit date: 2021-08-31 00:03:23 +0100
Commit message: fix(json-deserializer): fixed codeQL vulnerability

Removed the regex used to identify a JSON mime-type as this was potentially vulnerable to
'Regular expression Denial of Service attacks' and making changes to make the regex more restrictive (but compliant with RFC 6838) did not pass tests for this issue online.

This now matches based on the last segment of a mimetype being json or json; this should match all the expected mime types.

Updated unit-tests with additional tests for a variety of compatible mime types.

Diff: diff --git a/packages/json-deserializer/src/JsonDeserializer.test.ts b/packages/json-deserializer/src/JsonDeserializer.test.ts
index 9481133..2748622 100644
--- a/packages/json-deserializer/src/JsonDeserializer.test.ts
+++ b/packages/json-deserializer/src/JsonDeserializer.test.ts
@@ -27,20 +27,27 @@ describe("jsonDeserializer", () => {
         andArrays: ["ofStrings", 42, null],
       },
     },
   };
 
-  it("deserializes objects if the event has a json content-type header", async () => {
-    const event: APIGatewayProxyEvent = createEvent({
-      headers: { "Content-Type": "application/json" },
-      body: JSON.stringify(testObject),
-    });
+  describe("deserializes objects the event has a valid json content-type header:", () => {
+    it.each([
+      ["subtype is json", "application/json"],
+      ["subtype structured syntax specifier is json", "application/ld+json"],
+      ["semicolon is appended", "application/json;"],
+      ["whitespace is appended", "application/json;   "],
+    ])("%s (%p)", async (_string, mimeType: string) => {
+      const event: APIGatewayProxyEvent = createEvent({
+        headers: { "Content-Type": mimeType },
+        body: JSON.stringify(testObject),
+      });
 
-    await handlerWithMiddleware(event, createContext());
-    expect(processedEvent.bodyObject).toMatchObject(
-      JSON.parse(JSON.stringify(testObject))
-    );
+      await handlerWithMiddleware(event, createContext());
+      expect(processedEvent.bodyObject).toMatchObject(
+        JSON.parse(JSON.stringify(testObject))
+      );
+    });
   });
 
   describe("sets the request bodyObject to null if:", () => {
     it.each([
       ["null headers", null],
diff --git a/packages/json-deserializer/src/JsonDeserializer.ts b/packages/json-deserializer/src/JsonDeserializer.ts
index af98ec8..1dacc7f 100644
--- a/packages/json-deserializer/src/JsonDeserializer.ts
+++ b/packages/json-deserializer/src/JsonDeserializer.ts
@@ -1,17 +1,14 @@
 import { PromiseHandler } from "@lambda-middleware/utils";
-import debugFactory, { IDebugger } from "debug";
 import {
   APIGatewayProxyEvent,
   APIGatewayProxyResult,
   Context,
 } from "aws-lambda";
 import { APIGatewayProxyObjectEvent } from "./types/APIGatewayProxyObjectEvent";
 import { RequestBodyNotJsonError } from "./customErrors/RequestBodyNotJsonError";
 
-const logger: IDebugger = debugFactory("@lambda-middleware/json-serializer");
-
 export const jsonDeserializer = <E extends APIGatewayProxyEvent>() => (
   handler: PromiseHandler<APIGatewayProxyObjectEvent<E>, APIGatewayProxyResult>
 ) => async (event: E, context: Context): Promise<APIGatewayProxyResult> => {
   const bodyObject = deserializeBody(event);
   return await handler({ ...event, bodyObject }, context);
@@ -48,8 +45,15 @@ const isJsonMimeType = (event: APIGatewayProxyEvent) => {
 
   if (!contentTypeHeader) {
     return false;
   }
 
-  const mimePattern = /^application\/(.+\+)?json(;.*)?$/;
-  return mimePattern.test(contentTypeHeader);
+  const mimeParts = contentTypeHeader.split("/");
+
+  if (mimeParts.length != 2) {
+    return false;
+  }
+
+  const lastSubtypePart = mimeParts[1].toLowerCase().split("+").pop()?.trim();
+
+  return lastSubtypePart === "json" || lastSubtypePart === "json;";
 };

Vulnerable code:   it("deserializes objects if the event has a json content-type header", async () => {
    const event: APIGatewayProxyEvent = createEvent({
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(testObject),
    });
    await handlerWithMiddleware(event, createContext());
    expect(processedEvent.bodyObject).toMatchObject(
      JSON.parse(JSON.stringify(testObject))
    );
import debugFactory, { IDebugger } from "debug";
const logger: IDebugger = debugFactory("@lambda-middleware/json-serializer");

  const mimePattern = /^application\/(.+\+)?json(;.*)?$/;
  return mimePattern.test(contentTypeHeader);
Fixed code:   describe("deserializes objects the event has a valid json content-type header:", () => {
    it.each([
      ["subtype is json", "application/json"],
      ["subtype structured syntax specifier is json", "application/ld+json"],
      ["semicolon is appended", "application/json;"],
      ["whitespace is appended", "application/json;   "],
    ])("%s (%p)", async (_string, mimeType: string) => {
      const event: APIGatewayProxyEvent = createEvent({
        headers: { "Content-Type": mimeType },
        body: JSON.stringify(testObject),
      });
      await handlerWithMiddleware(event, createContext());
      expect(processedEvent.bodyObject).toMatchObject(
        JSON.parse(JSON.stringify(testObject))
      );
    });
  const mimeParts = contentTypeHeader.split("/");

  if (mimeParts.length != 2) {
    return false;
  }

  const lastSubtypePart = mimeParts[1].toLowerCase().split("+").pop()?.trim();

  return lastSubtypePart === "json" || lastSubtypePart === "json;";