CVE-2024-5126: Improper Access Control (CWE-284)
value: An improper access control vulnerability exists in the lunary-ai/lunary repository, specifically within the versions.patch functionality for updating prompts. Affected versions include 1.2.2 up to but not including 1.2.25. The vulnerability allows unauthorized users to update prompt details due to insufficient access control checks. This issue was addressed and fixed in version 1.2.25.
CWE description: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
Published: 2024-06-06T19:16Z
Repository: https://github.com/lunary-ai/lunary
Language: TypeScript
Files changed: packages/backend/src/api/v1/template-versions.ts, packages/backend/src/api/v1/users.ts
Commit date: 2024-05-19 14:35:54 -0300
Commit message: fix: backend plan check (#329)


Diff: diff --git a/packages/backend/src/api/v1/template-versions.ts b/packages/backend/src/api/v1/template-versions.ts
index 09d91efe..8a940fb4 100644
--- a/packages/backend/src/api/v1/template-versions.ts
+++ b/packages/backend/src/api/v1/template-versions.ts
@@ -2,10 +2,11 @@ import sql from "@/src/utils/db"
 import Router from "koa-router"
 import { Context } from "koa"
 import postgres from "postgres"
 import { unCamelObject } from "@/src/utils/misc"
 import { checkAccess } from "@/src/utils/authorization"
+import { z } from "zod"
 
 const versions = new Router({
   prefix: "/template_versions",
 })
 
@@ -66,28 +67,49 @@ versions.get("/:id", async (ctx: Context) => {
 
 versions.patch(
   "/:id",
   checkAccess("prompts", "update"),
   async (ctx: Context) => {
-    const { content, extra, testValues, isDraft } = ctx.request.body as {
-      id: string
-      content: any[]
-      extra: any
-      testValues: any
-      isDraft: boolean
-    }
+    const bodySchema = z.object({
+      content: z.array(z.any()),
+      extra: z.any(),
+      testValues: z.any(),
+      isDraft: z.boolean(),
+    })
+
+    const { content, extra, testValues, isDraft } = bodySchema.parse(
+      ctx.request.body,
+    )
 
     const [templateVersion] = await sql`
-    update template_version set
-      content = ${sql.json(content)},
-      extra = ${sql.json(unCamelObject(extra))},
-      test_values = ${sql.json(testValues)},
-      is_draft = ${isDraft}
-    where id = ${ctx.params.id}
-    returning *
-  `
+      select
+        *
+      from
+        template_version tv
+        left join template t on tv.template_id = t.id
+        left join project p on t.project_id = p.id 
+      where
+        tv.id = ${ctx.params.id}
+        and p.org_id = ${ctx.state.orgId}
+    `
+
+    if (!templateVersion) {
+      ctx.throw(401, "You don't have access to this template")
+    }
 
-    ctx.body = templateVersion
+    const [updatedTemplateVersion] = await sql`
+      update template_version 
+      set
+        content = ${sql.json(content)},
+        extra = ${sql.json(unCamelObject(extra))},
+        test_values = ${sql.json(testValues)},
+        is_draft = ${isDraft}
+      where 
+        id = ${ctx.params.id}
+      returning *
+    `
+
+    ctx.body = updatedTemplateVersion
   },
 )
 
 export default versions
diff --git a/packages/backend/src/api/v1/users.ts b/packages/backend/src/api/v1/users.ts
index 491ce58e..6e7db8a2 100644
--- a/packages/backend/src/api/v1/users.ts
+++ b/packages/backend/src/api/v1/users.ts
@@ -181,13 +181,25 @@ users.post("/", checkAccess("teamMembers", "create"), async (ctx: Context) => {
 
   const [org] = await sql`
     select name, plan from org where id = ${orgId}
   `
 
+  if (
+    role !== "member" &&
+    role !== "admin" &&
+    (org.plan === "free" || org.plan === "pro")
+  ) {
+    ctx.throw(
+      401,
+      "Your plan doesn't allow you to access granular access control.",
+    )
+  }
+
   const [orgUserCountResult] = await sql`
     select count(*) from account where org_id = ${orgId}
   `
+
   const orgUserCount = orgUserCountResult.count
 
   const token = await signJWT({ email, orgId }, FIFTEEN_DAYS)
   const userToInsert = {
     email,

Vulnerable code:     const { content, extra, testValues, isDraft } = ctx.request.body as {
      id: string
      content: any[]
      extra: any
      testValues: any
      isDraft: boolean
    }
    update template_version set
      content = ${sql.json(content)},
      extra = ${sql.json(unCamelObject(extra))},
      test_values = ${sql.json(testValues)},
      is_draft = ${isDraft}
    where id = ${ctx.params.id}
    returning *
  `
    ctx.body = templateVersion
Fixed code: import { z } from "zod"
    const bodySchema = z.object({
      content: z.array(z.any()),
      extra: z.any(),
      testValues: z.any(),
      isDraft: z.boolean(),
    })

    const { content, extra, testValues, isDraft } = bodySchema.parse(
      ctx.request.body,
    )
      select
        *
      from
        template_version tv
        left join template t on tv.template_id = t.id
        left join project p on t.project_id = p.id 
      where
        tv.id = ${ctx.params.id}
        and p.org_id = ${ctx.state.orgId}
    `

    if (!templateVersion) {
      ctx.throw(401, "You don't have access to this template")
    }
    const [updatedTemplateVersion] = await sql`
      update template_version 
      set
        content = ${sql.json(content)},
        extra = ${sql.json(unCamelObject(extra))},
        test_values = ${sql.json(testValues)},
        is_draft = ${isDraft}
      where 
        id = ${ctx.params.id}
      returning *
    `

    ctx.body = updatedTemplateVersion
  if (
    role !== "member" &&
    role !== "admin" &&
    (org.plan === "free" || org.plan === "pro")
  ) {
    ctx.throw(
      401,
      "Your plan doesn't allow you to access granular access control.",
    )
  }