+ "details": "### Summary\nThis vulnerability allows a user i.e a free plan user to get more than the desired subdomains due to lack of db transaction lock mechanisms in `https://github.com/akinloluwami/outray/blob/main/apps/web/src/routes/api/%24orgSlug/subdomains/index.ts`\n\n### Details\n- The affected code-:\n\n```ts\n//Race condition\n const [subscription] = await db\n .select()\n .from(subscriptions)\n .where(eq(subscriptions.organizationId, organization.id));\n\n const currentPlan = subscription?.plan || \"free\";\n const planLimits = getPlanLimits(currentPlan as any);\n const subdomainLimit = planLimits.maxSubdomains;\n\n const existingSubdomains = await db\n .select()\n .from(subdomains)\n .where(eq(subdomains.organizationId, organization.id));\n\n if (existingSubdomains.length >= subdomainLimit) {\n return json(\n {\n error: `Subdomain limit reached. The ${currentPlan} plan allows ${subdomainLimit} subdomain${subdomainLimit > 1 ? \"s\" : \"\"}.`,\n },\n { status: 403 },\n );\n }\n\n const existing = await db\n .select()\n .from(subdomains)\n .where(eq(subdomains.subdomain, subdomain))\n .limit(1);\n\n if (existing.length > 0) {\n return json({ error: \"Subdomain already taken\" }, { status: 409 });\n }\n\n const [newSubdomain] = await db\n .insert(subdomains)\n .values({\n id: crypto.randomUUID(),\n subdomain,\n organizationId: organization.id,\n userId: session.user.id,\n })\n .returning();\n```\n\n- The first part of the code checks the user plan and determine his/her existing_domains without locking the transaction and allowing it to run.\n```ts\nconst existingSubdomains = await db\n .select()\n .from(subdomains)\n .where(eq(subdomains.organizationId, organization.id));\n```\n\n- The other part of the code checks if the desired domain is more than the limit.\n\n```ts\nif (existingSubdomains.length >= subdomainLimit) {\n return json(\n {\n error: `Subdomain limit reached. The ${currentPlan} plan allows ${subdomainLimit} subdomain${subdomainLimit > 1 ? \"s\" : \"\"}.`,\n },\n { status: 403 },\n );\n }\n```\n\n- Finally, it inserts the subdomain also after the whole check without locking transactions.\n\n```ts\nconst [newSubdomain] = await db\n .insert(subdomains)\n .values({\n id: crypto.randomUUID(),\n subdomain,\n organizationId: organization.id,\n userId: session.user.id,\n })\n .returning();\n```\n- An attacker can exploit this by making parallel requests to the same endpoint and if the second request reads row `subdomains` before the `INSERT` statement of request one is made.It allows the attacker to act on a not yet updated row which bypasses the checks and allow the attacker to get more subdomains.For example-:\n\n```\n Parallel request 1 Parallel Request 2 \n | |\nchecks for Checks the not yet updated\navailable subdomain row and bypasses the logic checks\nand determines if it is more than limit\n | |\nInserts subdomain and calls it a day Also inserts the subdomain\n```\n- The attack focuses on exploiting the race window between reading and writing the db rows.\n\n### PoC\n\n- Intercept with Burp proxy,pass to `Repeater` and create multiple requests in a single batch with different subdomain names as seen below. Lastly, send the requests in `parallel`.\n\n<img width=\"1844\" height=\"855\" alt=\"image\" src=\"https://github.com/user-attachments/assets/f46d5993-31bd-4b96-902a-b2de5b0518bd\" />\n\n- Result-:\n\n<img width=\"1905\" height=\"977\" alt=\"image\" src=\"https://github.com/user-attachments/assets/4c877de2-4b55-46f4-9f1c-78590dfebefc\" />\n\n\n### Impact\nThe vulnerability provides an infiinite supply of domains to users bypassing the need for subscription",
0 commit comments