CVE-2026-22820
Outray cli is vulnerable to race conditions in tunnels creation
Outray openSource ngrok alternative. Prior to 0.1.5, a TOCTOU race condition vulnerability allows a user to exceed the set number of active tunnels in their subscription plan. This vulnerability is fixed in 0.1.5.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
6.3
Outray cli is vulnerable to race conditions in tunnels creation
Outray openSource ngrok alternative. Prior to 0.1.5, a TOCTOU race condition vulnerability allows a user to exceed the set number of active tunnels in their subscription plan. This vulnerability is fixed in 0.1.5.
- Record
CVE-2026-22820- CVSS 4.0
- 6.3 Medium
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 - Weakness
- CWE-367
- Updated
- 2026-11-02
EUVDEUVD-2026-2015
6.3
Outray openSource ngrok alternative. Prior to 0.1.5, a TOCTOU race condition vulnerability allows a user to exceed the set number of active tunnels in their subscription plan. This vulnerability is fixed in 0.1.5.
- Record
EUVD-2026-2015- CVSS 4.0
- 6.3 Medium
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 - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-3pqc-836w-jgr7
6.3
Outray cli is vulnerable to race conditions in tunnels creation
Summary
A TOCTOU race condition vulnerability allows a user to exceed the set number of active tunnels in their subscription plan.
Details
Affected conponent: apps/web/src/routes/api/tunnel/register.ts
/tunnel/registerendpoint code-:
// Check if tunnel already exists in database
const [existingTunnel] = await db
.select()
.from(tunnels)
.where(eq(tunnels.url, tunnelUrl));
const isReconnection = !!existingTunnel;
console.log(
`[TUNNEL LIMIT CHECK] Org: ${organizationId}, Tunnel: ${tunnelId}`,
);
console.log(
`[TUNNEL LIMIT CHECK] Is Reconnection: ${isReconnection}`,
);
console.log(
`[TUNNEL LIMIT CHECK] Plan: ${currentPlan}, Limit: ${tunnelLimit}`,
);
// Check limits only for NEW tunnels (not reconnections)
if (!isReconnection) {
// Count active tunnels from Redis SET
const activeCount = await redis.scard(setKey);
console.log(
`[TUNNEL LIMIT CHECK] Active count in Redis: ${activeCount}`,
);
// The current tunnel is NOT yet in the online_tunnels set (added after successful registration)
// So we check if activeCount >= limit (not >)
if (activeCount >= tunnelLimit) {
console.log(
`[TUNNEL LIMIT CHECK] REJECTED - ${activeCount} >= ${tunnelLimit}`,
);
return json(
{
error: `Tunnel limit reached. The ${currentPlan} plan allows ${tunnelLimit} active tunnel${tunnelLimit > 1 ? "s" : ""}.`,
},
{ status: 403 },
);
}
console.log(
`[TUNNEL LIMIT CHECK] ALLOWED - ${activeCount} < ${tunnelLimit}`,
);
} else {
console.log(`[TUNNEL LIMIT CHECK] SKIPPED - Reconnection detected`);
}
if (existingTunnel) {
// Tunnel with this URL already exists, update lastSeenAt
await db
.update(tunnels)
.set({ lastSeenAt: new Date() })
.where(eq(tunnels.id, existingTunnel.id));
return json({
success: true,
tunnelId: existingTunnel.id,
});
}
// Create new tunnel record
const tunnelRecord = {
id: randomUUID(),
url: tunnelUrl,
userId,
organizationId,
name: name || null,
protocol,
remotePort: remotePort || null,
lastSeenAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
await db.insert(tunnels).values(tunnelRecord);
return json({ success: true, tunnelId: tunnelRecord.id });
} catch (error) {
console.error("Tunnel registration error:", error);
return json({ error: "Internal server error" }, { status: 500 });
}
- It checks if the tunnel exists in the database.
// Check if tunnel already exists in database
const [existingTunnel] = await db
.select()
.from(tunnels)
.where(eq(tunnels.url, tunnelUrl));
const isReconnection = !!existingTunnel;
- Limit is checked here-:
// Check limits only for NEW tunnels (not reconnections)
if (!isReconnection) {
// Count active tunnels from Redis SET
const activeCount = await redis.scard(setKey);
console.log(
`[TUNNEL LIMIT CHECK] Active count in Redis: ${activeCount}`,
);
- Redis is checked for existing tunnel to check for reconnection.
// Check limits only for NEW tunnels (not reconnections)
if (!isReconnection) {
// Count active tunnels from Redis SET
const activeCount = await redis.scard(setKey);
console.log(
…
- Record
GHSA-3pqc-836w-jgr7- CVSS 4.0
- 6.3 Medium
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 - CVSS 3.1
- 3.7 Low
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N - Weakness
- CWE-367
- Updated
- 2026-11-02
NVD
6.3
Outray openSource ngrok alternative. Prior to 0.1.5, a TOCTOU race condition vulnerability allows a user to exceed the set number of active tunnels in their subscription plan. This vulnerability is fixed in 0.1.5.
- Record
CVE-2026-22820- CVSS 4.0
- 6.3 Medium
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 3.7 Low
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N - Weakness
- CWE-367
- Updated
- 2026-11-02
OSV
6.3
Outray cli is vulnerable to race conditions in tunnels creation
Outray openSource ngrok alternative. Prior to 0.1.5, a TOCTOU race condition vulnerability allows a user to exceed the set number of active tunnels in their subscription plan. This vulnerability is fixed in 0.1.5.
- Record
CVE-2026-22820- CVSS 4.0
- 6.3 Medium
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 - Weakness
- CWE-367
- Updated
- 2026-11-07
OSVGHSA-3pqc-836w-jgr7
6.3
Outray cli is vulnerable to race conditions in tunnels creation
Summary
A TOCTOU race condition vulnerability allows a user to exceed the set number of active tunnels in their subscription plan.
Details
Affected conponent: apps/web/src/routes/api/tunnel/register.ts
/tunnel/registerendpoint code-:
// Check if tunnel already exists in database
const [existingTunnel] = await db
.select()
.from(tunnels)
.where(eq(tunnels.url, tunnelUrl));
const isReconnection = !!existingTunnel;
console.log(
`[TUNNEL LIMIT CHECK] Org: ${organizationId}, Tunnel: ${tunnelId}`,
);
console.log(
`[TUNNEL LIMIT CHECK] Is Reconnection: ${isReconnection}`,
);
console.log(
`[TUNNEL LIMIT CHECK] Plan: ${currentPlan}, Limit: ${tunnelLimit}`,
);
// Check limits only for NEW tunnels (not reconnections)
if (!isReconnection) {
// Count active tunnels from Redis SET
const activeCount = await redis.scard(setKey);
console.log(
`[TUNNEL LIMIT CHECK] Active count in Redis: ${activeCount}`,
);
// The current tunnel is NOT yet in the online_tunnels set (added after successful registration)
// So we check if activeCount >= limit (not >)
if (activeCount >= tunnelLimit) {
console.log(
`[TUNNEL LIMIT CHECK] REJECTED - ${activeCount} >= ${tunnelLimit}`,
);
return json(
{
error: `Tunnel limit reached. The ${currentPlan} plan allows ${tunnelLimit} active tunnel${tunnelLimit > 1 ? "s" : ""}.`,
},
{ status: 403 },
);
}
console.log(
`[TUNNEL LIMIT CHECK] ALLOWED - ${activeCount} < ${tunnelLimit}`,
);
} else {
console.log(`[TUNNEL LIMIT CHECK] SKIPPED - Reconnection detected`);
}
if (existingTunnel) {
// Tunnel with this URL already exists, update lastSeenAt
await db
.update(tunnels)
.set({ lastSeenAt: new Date() })
.where(eq(tunnels.id, existingTunnel.id));
return json({
success: true,
tunnelId: existingTunnel.id,
});
}
// Create new tunnel record
const tunnelRecord = {
id: randomUUID(),
url: tunnelUrl,
userId,
organizationId,
name: name || null,
protocol,
remotePort: remotePort || null,
lastSeenAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
await db.insert(tunnels).values(tunnelRecord);
return json({ success: true, tunnelId: tunnelRecord.id });
} catch (error) {
console.error("Tunnel registration error:", error);
return json({ error: "Internal server error" }, { status: 500 });
}
- It checks if the tunnel exists in the database.
// Check if tunnel already exists in database
const [existingTunnel] = await db
.select()
.from(tunnels)
.where(eq(tunnels.url, tunnelUrl));
const isReconnection = !!existingTunnel;
- Limit is checked here-:
// Check limits only for NEW tunnels (not reconnections)
if (!isReconnection) {
// Count active tunnels from Redis SET
const activeCount = await redis.scard(setKey);
console.log(
`[TUNNEL LIMIT CHECK] Active count in Redis: ${activeCount}`,
);
- Redis is checked for existing tunnel to check for reconnection.
// Check limits only for NEW tunnels (not reconnections)
if (!isReconnection) {
// Count active tunnels from Redis SET
const activeCount = await redis.scard(setKey);
console.log(
…
- Record
GHSA-3pqc-836w-jgr7- CVSS 4.0
- 6.3 Medium
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 - CVSS 3.1
- 3.7 Low
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N - Weakness
- CWE-367
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
outraynpm |
before 0.1.5 | 0.1.5 |
History
| Recorded | Changed | Sources |
|---|---|---|
| facts | OSV | |
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-22820 is a medium vulnerability (CVSS 6.3) in Akinloluwami Outray, published January 13, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.20%, the 10th percentile. Fixed in outray 0.1.5.
References
github.com/outray-tunnel/outray/security/advisories/GHSA-3pqc-836w-jgr7
github.com/outray-tunnel/outray/commit/08c61495761349e7fd2965229c3faa8d7b1c1581
github.com/akinloluwami/outray/security/advisories/GHSA-3pqc-836w-jgr7
nvd.nist.gov/vuln/detail/CVE-2026-22820
github.com/advisories/GHSA-3pqc-836w-jgr7
github.com/CVEProject/cvelistV5/tree/main/cves/2026/22xxx/CVE-2026-22820.json
github.com/akinloluwami/outray
Left out here: the rest of 2 source descriptions. The JSON record has them.