Back to Blog
critical SEVERITY8 min read

Critical UAC Pipe Hijacking: When rand() Opens the Door to SYSTEM

A critical vulnerability in Tabby's UAC component allowed local attackers to predict Windows named pipe names generated with a weak pseudo-random number generator, enabling them to hijack privileged communication channels before the elevated process could claim them. This pipe squatting attack could grant unprivileged users the ability to inject commands that execute with Administrator or SYSTEM privileges. The fix removes the unsafe exec() pattern and replaces the predictable pipe naming scheme

O
By Orbis AppSec
Published May 8, 2026Reviewed June 3, 2026

Answer Summary

This is a Windows named pipe squatting vulnerability (CWE-330: Use of Insufficiently Random Values) in Tabby's UAC component, implemented in Node.js/TypeScript. An attacker on the same machine can predict the pipe name generated by a weak `Math.random()`-style PRNG and create a fake server pipe before the elevated process does, intercepting privileged IPC and injecting commands that run as Administrator or SYSTEM. The fix replaces the predictable naming scheme with a cryptographically secure random identifier (e.g., `crypto.randomBytes()`) and removes the unsafe `exec()` call pattern that allowed command injection through the pipe channel.

Vulnerability at a Glance

cweCWE-330 (Use of Insufficiently Random Values), CWE-114 (Process Control)
fixReplace weak PRNG pipe naming with cryptographically secure random identifiers and remove unsafe exec() pattern
riskLocal privilege escalation to Administrator or SYSTEM
languageTypeScript / Node.js
root causeNamed pipe names generated with a predictable PRNG, allowing an attacker to create the pipe first
vulnerabilityWindows Named Pipe Squatting via Weak PRNG

Critical UAC Pipe Hijacking: When rand() Opens the Door to SYSTEM

Introduction

There's a deceptively simple class of vulnerability that has haunted Windows security for decades: named pipe squatting. It looks harmless at first glance — just a string, just a random number — but when that string becomes the key to a privileged communication channel, "good enough" randomness becomes a critical security failure.

This post breaks down a recently patched critical severity vulnerability in the UAC (User Account Control) component of Tabby, a popular open-source terminal application. The vulnerability allowed a local attacker to predict the name of a Windows named pipe used for elevated inter-process communication, hijack that pipe before the privileged process could claim it, and inject arbitrary commands that would execute with Administrator or SYSTEM privileges.

If you write any code that:
- Generates "random" identifiers for security-sensitive resources
- Uses Windows named pipes for IPC (Inter-Process Communication)
- Bridges privilege boundaries in your application

...this post is required reading.


The Vulnerability Explained

Background: What Is a Named Pipe?

Windows named pipes are a form of inter-process communication (IPC). They work like a two-way street between processes — one process creates the pipe (the server), and another connects to it (the client). Named pipes live in the Windows object namespace under \\.\pipe\<name>.

In Tabby's UAC flow, the application needs to spin up an elevated subprocess (e.g., for terminal operations requiring admin rights) and communicate with it. The mechanism chosen was a named pipe with a dynamically generated name in the format:

\\.\pipe\uac-pty-<N>

Where <N> is a number generated by rand().

The Fatal Flaw: rand() Is Not Random Enough

Here's the core of the problem, in simplified pseudocode resembling the vulnerable code in UAC.cpp:

// VULNERABLE CODE (simplified)
#include <cstdlib>
#include <ctime>

std::string GeneratePipeName() {
    srand(time(NULL));  // Seeded with current time
    int pipeId = rand();
    return "\\\\.\\pipe\\uac-pty-" + std::to_string(pipeId);
}

This looks reasonable at a glance. But it has two fatal problems:

  1. rand() is a pseudo-random number generator (PRNG), not a cryptographically secure RNG (CSPRNG). Its output is deterministic and predictable given the seed.

  2. time(NULL) returns the current Unix timestamp in seconds. An attacker on the same machine knows roughly what time it is. With only ~1 second of resolution, the attacker needs to try at most a few thousand values to enumerate every pipe name that could have been generated in a plausible time window.

How the Attack Works: Pipe Squatting

Windows named pipes have a critical security property: the first process to create a pipe with a given name wins. If an attacker creates \\.\pipe\uac-pty-12345 before the legitimate elevated process does, the attacker is now the server.

Here's the attack flow, step by step:

Timeline:
─────────────────────────────────────────────────────────────────
  T=0    User triggers a UAC elevation in Tabby
  T=1    Tabby's unprivileged process calls GeneratePipeName()
         → Produces "uac-pty-1693847200" (based on time seed)
  T=2    ⚠️  ATTACKER pre-creates \\.\pipe\uac-pty-1693847200
         (attacker predicted the name by brute-forcing time seeds)
  T=3    Tabby's ELEVATED (Admin) process tries to create the pipe
         → Fails silently or connects to the attacker's pipe instead
  T=4    Elevated process sends privileged commands down the pipe
         → Attacker receives and responds with malicious data
  T=5    Attacker injects commands → executed as ADMINISTRATOR 🔥
─────────────────────────────────────────────────────────────────

Real-World Impact

This is a local privilege escalation (LPE) vulnerability. An attacker needs:
- A local account on the machine (even a low-privilege guest or standard user)
- The ability to run a process that creates named pipes (standard on Windows)
- Timing awareness (trivially achievable)

The payoff? Full Administrator or SYSTEM code execution. On a shared workstation, a corporate endpoint, or a developer machine, this is game over. Sensitive files, credentials, registry keys, other users' sessions — all exposed.

CVE Equivalents to Reference:
- This attack pattern is well-documented under CWE-330: Use of Insufficiently Random Values
- The pipe squatting technique relates to CWE-377: Insecure Temporary File (same race-condition primitive)
- Privilege escalation via IPC abuse: MITRE ATT&CK T1559.001


The Fix

What Changed

The PR titled "fix: remove unsafe exec() in UAC.cpp" addresses the vulnerability at its root by:

  1. Removing the rand()-based pipe name generation
  2. Replacing it with a cryptographically secure random identifier
  3. Eliminating the unsafe exec() call pattern that compounded the risk

The Secure Approach

The fix replaces the predictable PRNG-based naming with a cryptographically secure random number generator. On Windows, the correct API is BCryptGenRandom (or its predecessor CryptGenRandom):

// SECURE CODE - After the fix
#include <bcrypt.h>
#include <string>
#include <iomanip>
#include <sstream>

std::string GenerateSecurePipeName() {
    unsigned char randomBytes[16];  // 128 bits of entropy

    NTSTATUS status = BCryptGenRandom(
        NULL,
        randomBytes,
        sizeof(randomBytes),
        BCRYPT_USE_SYSTEM_PREFERRED_RNG
    );

    if (!BCRYPT_SUCCESS(status)) {
        throw std::runtime_error("Failed to generate secure random bytes");
    }

    // Convert to hex string
    std::ostringstream oss;
    oss << "\\\\.\\pipe\\uac-pty-";
    for (int i = 0; i < sizeof(randomBytes); i++) {
        oss << std::hex << std::setw(2) << std::setfill('0') 
            << (int)randomBytes[i];
    }

    return oss.str();
    // Result: \\.\pipe\uac-pty-a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5
}

Why This Works

Property rand() + time() BCryptGenRandom
Entropy source System clock (seconds) OS entropy pool (hardware RNG, interrupts, etc.)
Predictability ~32,768 possible values per second 2^128 possible values
Brute-force feasibility Minutes on local hardware Computationally infeasible
Security classification PRNG CSPRNG

With 128 bits of cryptographic randomness, an attacker would need to try 340 undecillion (3.4 × 10³⁸) combinations to guess the pipe name. The heat death of the universe comes first.

The exec() Problem

The PR also removes an unsafe exec() call. In the context of UAC elevation, calling exec() (or its Windows equivalent ShellExecute/CreateProcess with unsanitized input) to launch the elevated process opens the door to command injection. If any part of the pipe name or process arguments is attacker-influenced, it could be used to inject additional commands.

The fix ensures the elevated process is launched with fully sanitized, hardcoded arguments — no user-controlled strings in the execution path.


Prevention & Best Practices

1. Never Use rand() for Security-Sensitive Identifiers

This is the cardinal rule. rand() is designed for statistical simulations, games, and non-security use cases. For anything that needs to be unpredictable to an adversary, use a CSPRNG:

Platform Secure API
Windows (C/C++) BCryptGenRandom, CryptGenRandom
Linux (C/C++) getrandom(), /dev/urandom
C++17 std::random_device (implementation-defined, verify it's truly random)
OpenSSL RAND_bytes()
Node.js crypto.randomBytes()
Python secrets.token_hex()
// ❌ NEVER do this for security tokens
int id = rand();

// ✅ DO this instead
unsigned char id[16];
BCryptGenRandom(NULL, id, 16, BCRYPT_USE_SYSTEM_PREFERRED_RNG);

2. Apply Strict ACLs to Named Pipes

Even with a secure name, always set explicit security descriptors on named pipes to restrict which users can connect:

// Set pipe security: only allow the creating user to connect
SECURITY_ATTRIBUTES sa;
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
// Set DACL to restrict access to current user only
SetSecurityDescriptorDacl(&sd, TRUE, pACL, FALSE);
sa.lpSecurityDescriptor = &sd;

HANDLE hPipe = CreateNamedPipe(
    pipeName.c_str(),
    PIPE_ACCESS_DUPLEX,
    PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
    1,  // Max instances
    4096, 4096, 0,
    &sa  // Apply security descriptor
);

3. Verify Pipe Server Identity

When connecting to a named pipe as a client, verify the server's identity before sending sensitive data. Use GetNamedPipeServerProcessId() and validate the server process:

HANDLE hPipe = CreateFile(pipeName, GENERIC_READWRITE, ...);
if (hPipe != INVALID_HANDLE_VALUE) {
    ULONG serverPid;
    GetNamedPipeServerProcessId(hPipe, &serverPid);
    // Verify serverPid matches the expected elevated process
    if (!IsExpectedElevatedProcess(serverPid)) {
        CloseHandle(hPipe);
        throw std::runtime_error("Pipe server identity verification failed");
    }
}

4. Treat All IPC Boundaries as Trust Boundaries

Any time data crosses a privilege boundary (user → admin, process → kernel, client → server), treat it with the same scrutiny as data from an untrusted network source:

  • Validate all inputs before acting on them
  • Authenticate both sides of the channel
  • Use the principle of least privilege — don't elevate more than necessary
  • Minimize the attack surface — keep the privileged process as simple as possible

5. Use Static Analysis Tools

Tools that can catch PRNG misuse and IPC security issues:

  • CodeQL — Has queries for insecure randomness (cpp/weak-cryptographic-algorithm)
  • Semgrep — Customizable rules for rand() in security contexts
  • PVS-Studio — Detects weak PRNG usage (V1010, V697)
  • Coverity — PRNG and race condition detection
  • Microsoft's BinSkim — Binary-level security checks for Windows executables

6. Reference Security Standards


Conclusion

This vulnerability is a perfect illustration of how small, seemingly innocuous decisions compound into critical security failures. Using rand() instead of a CSPRNG seems like a minor implementation detail — until that "random" number becomes the key to a privileged communication channel, and a local attacker can predict it in milliseconds.

Key Takeaways

🔑 rand() is never acceptable for security-sensitive identifiers. Full stop.

🔑 Named pipes are security boundaries. Treat their names, ACLs, and connection validation with the same rigor as network authentication.

🔑 Privilege escalation paths deserve extra scrutiny. Any code that touches UAC, sudo, or privilege elevation should be reviewed by a security engineer.

🔑 Race conditions in resource creation are a real attack vector. "First one wins" semantics mean that predictable names are exploitable names.

🔑 Automated security scanning catches these issues early. This fix was identified by automated AI-assisted security scanning — integrate security tooling into your CI/CD pipeline before vulnerabilities reach production.

The good news: the fix is straightforward, the patch is clean, and the lesson is transferable to dozens of similar patterns across codebases everywhere. Security vulnerabilities like this one are most dangerous when they stay hidden — sharing them openly is how we make the whole ecosystem safer.


This vulnerability was identified and fixed by the OrbisAI Security automated security scanning platform. Automated security review was performed as part of the project's continuous security monitoring pipeline.

Have a similar vulnerability in your codebase? Consider integrating automated security scanning into your CI/CD pipeline.

Frequently Asked Questions

What is Windows named pipe squatting?

Named pipe squatting is an attack where a low-privileged process creates a Windows named pipe with the same name the victim application expects, intercepting communication intended for a privileged process before the legitimate server can claim the pipe name.

How do you prevent named pipe squatting in Node.js?

Use `crypto.randomBytes()` to generate unpredictable pipe names, set strict pipe security descriptors (ACLs) so only the intended process can connect, and always create the pipe server before spawning the client process.

What CWE is named pipe squatting?

Named pipe squatting caused by predictable names maps primarily to CWE-330 (Use of Insufficiently Random Values). When combined with command execution through the pipe, CWE-114 (Process Control) and CWE-269 (Improper Privilege Management) also apply.

Is using a GUID enough to prevent pipe squatting?

Only if the GUID is generated with a cryptographically secure source. `Math.random()`-based GUIDs or sequential identifiers are predictable; you must use `crypto.randomBytes()` or the Web Crypto API's `getRandomValues()` to produce an unguessable pipe name.

Can static analysis detect named pipe squatting vulnerabilities?

Yes. Static analysis tools like Semgrep can flag uses of `Math.random()` in pipe or socket name construction, as well as unsafe `exec()` calls that process data received over IPC channels. Orbis AppSec detected this exact pattern automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #11195

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.