CVE-2024-37162: Insufficient Information (NVD-CWE-noinfo)
value: zsa is a library for building typesafe server actions in Next.js. All users are impacted. The zsa application transfers the parse error stack from the server to the client in production build mode. This can potentially reveal sensitive information about the server environment, such as the machine username and directory paths. An attacker could exploit this vulnerability to gain unauthorized access to sensitive server information. This information could be used to plan further attacks or gain a deeper understanding of the server infrastructure. This has been patched on `0.3.3`.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-06-07T15:15Z
Repository: https://github.com/IdoPesok/zsa
Language: Other
Files changed: examples/showcase/app/revalidate/component.tsx, examples/showcase/app/revalidate/page.tsx, examples/showcase/content/docs/examples/introduction/actions.ts, examples/showcase/content/docs/examples/introduction/increment-example.tsx, packages/zsa/src/errors.ts, packages/zsa/src/zod-safe-function.ts, tests/jest/__tests__/openapi.test.tsx
Commit date: 2024-06-06 13:38:07 -0700
Commit message: remove stack from errors

Diff: diff --git a/examples/showcase/app/revalidate/component.tsx b/examples/showcase/app/revalidate/component.tsx
new file mode 100644
index 0000000..6e7aeb4
--- /dev/null
+++ b/examples/showcase/app/revalidate/component.tsx
@@ -0,0 +1,17 @@
+"use client"
+
+import IncrementExample from "@/content/docs/examples/introduction/increment-example"
+import { useState } from "react"
+
+export default function RevalidateComponent() {
+  const [open, setOpen] = useState(false)
+  return (
+    <div>
+      {open ? (
+        <IncrementExample />
+      ) : (
+        <button onClick={() => setOpen(true)}>Open</button>
+      )}
+    </div>
+  )
+}
diff --git a/examples/showcase/app/revalidate/page.tsx b/examples/showcase/app/revalidate/page.tsx
new file mode 100644
index 0000000..4ac103a
--- /dev/null
+++ b/examples/showcase/app/revalidate/page.tsx
@@ -0,0 +1,15 @@
+import RevalidateComponent from "./component"
+
+export default async function RevalidatePage() {
+  // sleep for 2 seconds
+  await new Promise((resolve) => setTimeout(resolve, 1000))
+  const randomNumber = Math.floor(Math.random() * 10000)
+
+  return (
+    <div>
+      <h1>Revalidate Page</h1>
+      <p>Random number: {randomNumber}</p>
+      <RevalidateComponent />
+    </div>
+  )
+}
diff --git a/examples/showcase/content/docs/examples/introduction/actions.ts b/examples/showcase/content/docs/examples/introduction/actions.ts
index 44dd8be..4cf2049 100644
--- a/examples/showcase/content/docs/examples/introduction/actions.ts
+++ b/examples/showcase/content/docs/examples/introduction/actions.ts
@@ -1,7 +1,8 @@
 "use server"
 
+import { revalidatePath } from "next/cache"
 import z from "zod"
 import { createServerAction } from "zsa"
 
 export const incrementNumberAction = createServerAction()
   .input(
@@ -9,11 +10,14 @@ export const incrementNumberAction = createServerAction()
       number: z.number(),
     })
   )
   .handler(async ({ input }) => {
     await new Promise((resolve) => setTimeout(resolve, 500))
-    return input.number + 1
+
+    revalidatePath("/revalidate", "page")
+
+    return input.number + 2
   })
 
 export const plainAction = createServerAction().handler(async () => {
   return "hello world"
 })
diff --git a/examples/showcase/content/docs/examples/introduction/increment-example.tsx b/examples/showcase/content/docs/examples/introduction/increment-example.tsx
index 503050f..c578dcb 100644
--- a/examples/showcase/content/docs/examples/introduction/increment-example.tsx
+++ b/examples/showcase/content/docs/examples/introduction/increment-example.tsx
@@ -1,10 +1,11 @@
 "use client"
 
 import { Button } from "@/components/ui/button"
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
 import { useState } from "react"
+import { toast } from "sonner"
 import { useServerAction } from "zsa-react"
 import { incrementNumberAction } from "./actions"
 
 export default function IncrementExample() {
   const [counter, setCounter] = useState(0)
@@ -29,10 +30,11 @@ export default function IncrementExample() {
             if (err) {
               alert("got error : (")
               return
             }
 
+            toast("got data" + data)
             setCounter(data)
           }}
         >
           Invoke action
         </Button>
diff --git a/packages/zsa/src/errors.ts b/packages/zsa/src/errors.ts
index b41a1f7..ab9be9c 100644
--- a/packages/zsa/src/errors.ts
+++ b/packages/zsa/src/errors.ts
@@ -55,20 +55,18 @@ export class ZSAError extends Error {
 export type TZSAError<TInputSchema extends z.ZodType> = Error &
   (
     | {
         code: Exclude<keyof typeof ERROR_CODES, "INPUT_PARSE_ERROR">
         message?: string
-        stack: string
         data: string
         name: string
         fieldErrors?: undefined
         formErrors?: undefined
         formattedErrors?: undefined
       }
     | {
         message?: string
-        stack: string
         code: "INPUT_PARSE_ERROR"
         data: string
         name: string
         fieldErrors: z.inferFlattenedErrors<TInputSchema>["fieldErrors"]
         formErrors: z.inferFlattenedErrors<TInputSchema>["formErrors"]
diff --git a/packages/zsa/src/zod-safe-function.ts b/packages/zsa/src/zod-safe-function.ts
index 87f513b..52db260 100644
--- a/packages/zsa/src/zod-safe-function.ts
+++ b/packages/zsa/src/zod-safe-function.ts
@@ -510,11 +510,10 @@ export class ZodSafeFunction<
     return [
       null,
       {
         data: stringifyIfNeeded(customError.data),
         name: customError.name,
-        stack: JSON.stringify(customError.stack),
         message: stringifyIfNeeded(customError.message),
         code: customError.code,
         fieldErrors: flattenedErrors?.fieldErrors,
         formErrors: flattenedErrors?.formErrors,
         formattedErrors: formattedErrors as any,
diff --git a/tests/jest/__tests__/openapi.test.tsx b/tests/jest/__tests__/openapi.test.tsx
index 6c01656..fa6a7bc 100644
--- a/tests/jest/__tests__/openapi.test.tsx
+++ b/tests/jest/__tests__/openapi.test.tsx
@@ -163,11 +163,10 @@ describe("openapi", () => {
       expect(response.status).toBe(400)
 
       const json = await response.json()
       expect(json.code).toBe("INPUT_PARSE_ERROR")
       expect(json.message).toBeDefined()
-      expect(json.stack).not.toBeDefined()
     })
 
     it("should multiply two numbers [POST]", async () => {
       const { POST } = createRouteHandlers(openapiRouter)
 
@@ -385,11 +384,10 @@ describe("openapi", () => {
       expect(response.status).toBe(400)
 
       const json = await response.json()
       expect(json.code).toBe("INPUT_PARSE_ERROR")
       expect(json.message).toBeDefined()
-      expect(json.stack).toBeDefined()
       expect(json.name).toBeDefined()
     })
 
     it("it should fail to divide a number by zero and return a custom error [POST]", async () => {
       const { POST } = setupApiHandler(
@@ -417,11 +415,10 @@ describe("openapi", () => {
       expect(response.status).toBe(400)
 
       const json = await response.json()
       expect(json.code).toBe("INPUT_PARSE_ERROR")
       expect(json.message).toBeDefined()
-      expect(json.stack).not.toBeDefined()
     })
   })
 
   describe("next redirect", () => {
     it("throws the redirect error", async () => {

Vulnerable code:     return input.number + 1
        stack: string
        stack: string
        stack: JSON.stringify(customError.stack),
      expect(json.stack).not.toBeDefined()
      expect(json.stack).toBeDefined()
      expect(json.stack).not.toBeDefined()
Fixed code: "use client"

import IncrementExample from "@/content/docs/examples/introduction/increment-example"
import { useState } from "react"

export default function RevalidateComponent() {
  const [open, setOpen] = useState(false)
  return (
    <div>
      {open ? (
        <IncrementExample />
      ) : (
        <button onClick={() => setOpen(true)}>Open</button>
      )}
    </div>
  )
}
import RevalidateComponent from "./component"

export default async function RevalidatePage() {
  // sleep for 2 seconds
  await new Promise((resolve) => setTimeout(resolve, 1000))
  const randomNumber = Math.floor(Math.random() * 10000)

  return (
    <div>
      <h1>Revalidate Page</h1>
      <p>Random number: {randomNumber}</p>
      <RevalidateComponent />
    </div>
  )
}
import { revalidatePath } from "next/cache"

    revalidatePath("/revalidate", "page")

    return input.number + 2
import { toast } from "sonner"
            toast("got data" + data)