{"id":"CVE-2026-22819","aliases":["EUVD-2026-2016","GHSA-45hj-9x76-wp9g"],"status":"published","title":"Outray has a Race Condition in main/apps/web/src/routes/api/$orgSlug/subdomains/index.ts","description":"Outray openSource ngrok alternative. Prior to 0.1.5, this 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 main/apps/web/src/routes/api/$orgSlug/subdomains/index.ts. This vulnerability is fixed in 0.1.5.","severity":{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00245,"percentile":0.15811,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-366"],"published":"2026-01-13T21:53:30Z","modified":"2026-11-05T03:31:29Z","fixes":[{"ecosystem":"npm","package":"outray","fixed_in":["0.1.5"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"outray","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.1.5"}]}]},{"source":"osv","ecosystem":"npm","package":"outray","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.1.5"}]}]}],"products":[{"vendor":"akinloluwami","product":"outray"},{"vendor":"outray","product":"outray"}],"references":[{"url":"https://github.com/outray-tunnel/outray/security/advisories/GHSA-45hj-9x76-wp9g","tags":["x_refsource_CONFIRM","Exploit","Vendor Advisory","advisory","web"]},{"url":"https://github.com/outray-tunnel/outray/commit/73e8a09575754fb4c395438680454b2ec064d1d6","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://github.com/akinloluwami/outray/security/advisories/GHSA-45hj-9x76-wp9g","tags":["web"]},{"url":"https://github.com/outray-tunnel/outray/commit/08c61495761349e7fd2965229c3faa8d7b1c1581","tags":["web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-22819","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-45hj-9x76-wp9g"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/22xxx/CVE-2026-22819.json","tags":["advisory"]},{"url":"https://github.com/akinloluwami/outray","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-22819","title":"Outray has a Race Condition in main/apps/web/src/routes/api/$orgSlug/subdomains/index.ts","description":"Outray openSource ngrok alternative. Prior to 0.1.5, this 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 main/apps/web/src/routes/api/$orgSlug/subdomains/index.ts. This vulnerability is fixed in 0.1.5.","status":"published","published":"2026-01-14T18:04:33.426Z","modified":"2026-01-14T21:13:36.389Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H","level":"medium","assessor":"cna"}],"cwes":["CWE-366"],"updated_at":"2026-11-02T13:39:05.585774Z"}],"euvd":[{"id":"EUVD-2026-2016","description":"Outray openSource ngrok alternative. Prior to 0.1.5, this 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 main/apps/web/src/routes/api/$orgSlug/subdomains/index.ts. This vulnerability is fixed in 0.1.5.","status":"published","published":"2026-01-14T18:04:33Z","modified":"2026-01-14T21:13:36Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H","level":"medium"}],"updated_at":"2026-11-02T19:48:51.744142Z"}],"ghsa":[{"id":"GHSA-45hj-9x76-wp9g","title":"Outray has a Race Condition in the cli's webapp","description":"### 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 \u003e= subdomainLimit) {\n          return json(\n            {\n              error: `Subdomain limit reached. The ${currentPlan} plan allows ${subdomainLimit} subdomain${subdomainLimit \u003e 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 \u003e 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 \u003e= subdomainLimit) {\n          return json(\n            {\n              error: `Subdomain limit reached. The ${currentPlan} plan allows ${subdomainLimit} subdomain${subdomainLimit \u003e 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\u003cimg width=\"1844\" height=\"855\" alt=\"image\" src=\"https://github.com/user-attachments/assets/f46d5993-31bd-4b96-902a-b2de5b0518bd\" /\u003e\n\n- Result-:\n\n\u003cimg width=\"1905\" height=\"977\" alt=\"image\" src=\"https://github.com/user-attachments/assets/4c877de2-4b55-46f4-9f1c-78590dfebefc\" /\u003e\n\n\n### Impact\nThe vulnerability provides an infiinite supply of domains to users bypassing the need for subscription","status":"published","published":"2026-01-13T21:53:30Z","modified":"2026-01-14T19:50:51Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H","level":"medium"}],"cwes":["CWE-366"],"updated_at":"2026-11-02T14:39:17.333958Z"}],"nvd":[{"id":"CVE-2026-22819","description":"Outray openSource ngrok alternative. Prior to 0.1.5, this 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 main/apps/web/src/routes/api/$orgSlug/subdomains/index.ts. This vulnerability is fixed in 0.1.5.","status":"published","published":"2026-01-14T18:16:42.33Z","modified":"2026-06-17T10:20:29.18Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H","level":"medium","assessor":"secondary"},{"version":"3.1","score":3.1,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N","level":"low","assessor":"primary"}],"cwes":["CWE-366"],"updated_at":"2026-11-02T14:12:04.066712Z"}],"osv":[{"id":"CVE-2026-22819","title":"Outray has a Race Condition in main/apps/web/src/routes/api/$orgSlug/subdomains/index.ts","description":"Outray openSource ngrok alternative. Prior to 0.1.5, this 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 main/apps/web/src/routes/api/$orgSlug/subdomains/index.ts. This vulnerability is fixed in 0.1.5.","status":"published","published":"2026-01-14T18:04:33.426Z","modified":"2026-11-07T02:48:17.983404693Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H","level":"medium"}],"cwes":["CWE-366"],"updated_at":"2026-11-07T03:08:02.600137Z"},{"id":"GHSA-45hj-9x76-wp9g","title":"Outray has a Race Condition in the cli's webapp","description":"### 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 \u003e= subdomainLimit) {\n          return json(\n            {\n              error: `Subdomain limit reached. The ${currentPlan} plan allows ${subdomainLimit} subdomain${subdomainLimit \u003e 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 \u003e 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 \u003e= subdomainLimit) {\n          return json(\n            {\n              error: `Subdomain limit reached. The ${currentPlan} plan allows ${subdomainLimit} subdomain${subdomainLimit \u003e 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\u003cimg width=\"1844\" height=\"855\" alt=\"image\" src=\"https://github.com/user-attachments/assets/f46d5993-31bd-4b96-902a-b2de5b0518bd\" /\u003e\n\n- Result-:\n\n\u003cimg width=\"1905\" height=\"977\" alt=\"image\" src=\"https://github.com/user-attachments/assets/4c877de2-4b55-46f4-9f1c-78590dfebefc\" /\u003e\n\n\n### Impact\nThe vulnerability provides an infiinite supply of domains to users bypassing the need for subscription","status":"published","published":"2026-01-13T21:53:30Z","modified":"2026-02-03T02:58:52.332916Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H","level":"medium"}],"cwes":["CWE-366"],"updated_at":"2026-11-02T15:46:36.434231Z"}]}}
