Skip to content

CWE-421: Race Condition During Access to Alternate Channel

Overview

This weakness occurs when a product opens an alternate communication channel - a secondary port, named pipe, socket, or callback URL - intended for one specific authorized user, but does not verify or restrict who connects to it first. An attacker who wins the race to connect before the legitimate user can hijack the channel and be served as the session it was opened for.

Relationship to Other CWEs

Do not confuse this with CWE-364 (Signal Handler Race Condition), which is about asynchronous signal delivery interrupting shared-state access, not about competing to connect to a network/IPC channel. CWE-421 is a specific case of the general race-condition weakness (CWE-362) applied to session/channel setup.

OWASP Classification

A01:2025 - Broken Access Control

Risk

High: An attacker who wins the race takes over a session intended for another user - reading the data meant for that client, or invoking privileged functionality without ever authenticating. Documented cases include the FTP "pizza thief" vulnerability (CVE-1999-0351), where an attacker connected to a data port meant for another client's session, and Windows named-pipe hijacking during authentication (CVE-2003-0230).

Remediation Steps

Core Principle: Never treat "whoever connects first" as an implicit authentication mechanism - verify the connecting party's identity on the alternate channel itself.

Trace the Data Path

  • Source: Code that opens a secondary channel for a specific client - a passive-mode FTP data port, a named pipe or Unix socket for IPC, a callback URL, a temporary rendezvous port
  • Sink: The code that accepts a connection on that channel and treats it as belonging to the session that requested it
  • Missing control: No binding between the channel and the identity of the peer that is supposed to use it, and no check that rejects unexpected connections

Bind the Channel to the Requesting Session (Primary Defense)

  • Generate a unique, cryptographically unpredictable per-session token or channel name, and require it before granting access on the alternate channel
  • Verify the connecting peer's identity where the OS provides a way to do so - pin the data connection's source address to the control connection's peer, or check OS-level peer credentials (e.g. SO_PEERCRED on Unix sockets, GetNamedPipeClientProcessId on Windows named pipes) before trusting the connection
  • Prefer OS primitives that create the channel with restricted access atomically (owner-only permissions set at creation) over "create the channel, then restrict permissions," which leaves a window where anyone can connect. On Unix sockets that means creating the socket under a directory only the owner can traverse, or setting umask before bind() rather than calling chmod after it; on Windows named pipes it means passing a restrictive security descriptor through lpSecurityAttributes rather than accepting the default, whose ACL grants read access to Everyone and the anonymous account
  • Guard against the squatter as well as the interloper. The two hijacks look different: one connects to your channel before the intended peer, and the other creates the channel under the expected name before you do, so the legitimate client connects to the attacker. The second is why "fail if the name already exists" is a control rather than an error-handling detail

On Windows, FILE_FLAG_FIRST_PIPE_INSTANCE answers "was I first", and only that. Measured on Windows 11 with CreateNamedPipeW: where an instance of the name already existed, created by a caller that did not pass the flag, a subsequent create that did pass it failed with ERROR_ACCESS_DENIED (5). That is the useful result, and it is broader than Microsoft's own sentence for the flag, which describes only the case where both callers pass it. A server can therefore detect a squatter that has no reason to cooperate.

What the flag does not do is protect the name afterwards. In the same test, a server that created the pipe with the flag was then joined by a second instance created without it, and that call succeeded. Adding an instance needs FILE_CREATE_PIPE_INSTANCE access, which the pipe's DACL decides, so the security descriptor is the control for the second half and the flag is the control for the first. Both of those runs were the same user, so the second result shows the flag alone does not deny a later instance; it does not establish what a different user could do against the default ACL. Set the descriptor explicitly rather than reason about that default.

Minimize and Monitor the Race Window

  • Reduce the time between opening the channel and validating the connecting party
  • Apply a short timeout so the channel does not sit open indefinitely waiting for the legitimate peer
  • Log rejected or unexpected connections on alternate channels so hijack attempts are detectable

Prefer Designs That Avoid a Second Channel

  • Multiplex requests over the already-authenticated primary connection instead of opening a new channel per request, where the protocol allows it
  • If a secondary channel is unavoidable, make its name or address unpredictable and single-use rather than fixed or sequentially assigned

Test the Fix

  • Attempt to connect to the alternate channel before the legitimate client connects; verify the connection is rejected, and that the legitimate client still succeeds afterwards - a fix that closes the channel on the first unauthorized attempt has turned the hijack into a denial of service
  • Create the channel's name yourself before the application starts - a named pipe, a Unix socket path - and verify the application refuses to start or fails the operation rather than adopting the name that already exists
  • Attempt to guess or brute-force the channel's name/port if it is meant to be unpredictable; confirm access is denied
  • Verify the channel enforces a timeout and rejects late or replayed connection attempts
  • Re-scan with the security scanner to confirm the finding is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
server.open_secondary_channel(fixed_port)
connection = secondary_channel.accept()   // no identity check
serve(connection, as_user = last_authenticated_session)
// Attack: attacker connects to fixed_port before the real client
// Result: attacker is treated as the authenticated session

Why this is vulnerable: the authorization happened on the primary channel and the secondary channel has no way to see it. What connects the two is time - the assumption that the next connection to arrive belongs to the client that just authenticated - and time is not an identity. An attacker racing a legitimate client only has to be faster, not authorized.

A fixed port makes that race trivial to win, because it removes the only thing the attacker would otherwise have to discover. They can hold a connection attempt open in advance and take the channel the moment it opens, while the real client cannot connect until it has been told to. Randomising the port narrows the window without closing it, since a port is not a credential and can be found by scanning. What closes it is carrying the authorization onto the second channel explicitly: a single-use token issued over the authenticated primary channel and required before the secondary connection is served, so that being first stops being sufficient.

Secure Patterns

// SECURE - pseudo-code
token = generate_unpredictable_token(session)
server.open_secondary_channel(unpredictable_address, expects_token = token, timeout = short)
connection = secondary_channel.accept_with_token(token)
if connection is null:
    reject()  // timed out or wrong token
serve(connection, as_user = session)

Why this works: Binding the channel to an unpredictable, session-specific token means connecting first is no longer enough. The attacker also has to guess the token, which is infeasible when it comes from a cryptographically secure random source. The short timeout closes the race window instead of leaving the channel open indefinitely for anyone to claim.

Additional Resources