CVE-2024-5127: Improper Access Control (CWE-284)
value: In lunary-ai/lunary versions 1.2.2 through 1.2.25, an improper access control vulnerability allows users on the Free plan to invite other members and assign them any role, including those intended for Paid and Enterprise plans only. This issue arises due to insufficient backend validation of roles and permissions, enabling unauthorized users to join a project and potentially exploit roles and permissions not intended for their use. The vulnerability specifically affects the Team feature, where the backend fails to validate whether a user has paid for a plan before allowing them to send invite links with any role assigned. This could lead to unauthorized access and manipulation of project settings or data.
CWE description: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
Published: 2024-06-06T18:15Z
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.",
    )
  }