{"id":"CVE-2026-22819","aliases":["GHSA-45hj-9x76-wp9g"],"url":"https://o3.security/vulnerability/CVE-2026-22819","summary":"Outray has a Race Condition in main/apps/web/src/routes/api/$orgSlug/subdomains/index.ts","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","published":"2026-01-14T18:04:33.426Z","modified":"2026-08-12T03:51:30.619806759Z","cvss":{"score":5.9,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"outray","fixedVersion":"0.1.5"}],"fix":{"url":"https://github.com/outray-tunnel/outray/commit/73e8a09575754fb4c395438680454b2ec064d1d6","label":"outray-tunnel/outray@73e8a09"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/22xxx/CVE-2026-22819.json"},{"type":"ADVISORY","url":"https://github.com/outray-tunnel/outray/security/advisories/GHSA-45hj-9x76-wp9g"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-22819"},{"type":"FIX","url":"https://github.com/outray-tunnel/outray/commit/73e8a09575754fb4c395438680454b2ec064d1d6"},{"type":"WEB","url":"https://github.com/akinloluwami/outray/security/advisories/GHSA-45hj-9x76-wp9g"},{"type":"WEB","url":"https://github.com/outray-tunnel/outray/commit/08c61495761349e7fd2965229c3faa8d7b1c1581"},{"type":"PACKAGE","url":"https://github.com/akinloluwami/outray"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:30.619806759Z"}}