Skip to content

Security Audit: Core Library Review (92 files) for CodeRabbit#2

Open
RahulRaj272727 wants to merge 1 commit intoreview-basefrom
ixwebsocket-core-review
Open

Security Audit: Core Library Review (92 files) for CodeRabbit#2
RahulRaj272727 wants to merge 1 commit intoreview-basefrom
ixwebsocket-core-review

Conversation

@RahulRaj272727
Copy link
Owner

@RahulRaj272727 RahulRaj272727 commented Jan 29, 2026

Purpose

This PR contains ONLY the core IXWebSocket library files (ixwebsocket/ directory) for security review by CodeRabbit AI.

File Count: 92 files (under 100 limit)

Review Scope

  • All 39 .cpp implementation files
  • All 53 .h header files
  • Core WebSocket, HTTP, TLS/SSL implementation

Security Focus Areas

@coderabbitai full review

Please audit for:

  • Memory safety (buffer overflows, use-after-free)
  • Input validation vulnerabilities
  • TLS/SSL implementation security (OpenSSL, MbedTLS, AppleSSL backends)
  • WebSocket protocol compliance (RFC 6455)
  • Cryptographic weaknesses (RNG quality)
  • DoS vectors (unbounded loops, memory exhaustion)
  • Race conditions and thread safety
  • SSRF/path traversal in HTTP handling

Summary by CodeRabbit

Release Notes

  • New Features
    • Added complete WebSocket client and server implementation with automatic reconnection and heartbeat support.
    • Added HTTP client and server components with header parsing and response handling.
    • Added per-message compression support with configurable deflate options.
    • Added TLS/SSL support with multiple backend implementations.
    • Added networking utilities: DNS lookup with timeout, free port discovery, UDP sockets.
    • Added data compression: gzip encode/decode and Base64 conversion.
    • Added URL parsing, UUID generation, and HTTP parameter serialization.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • ✅ Full review completed - (🔄 Check again to review again)

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In `@ixwebsocket/IXBase64.h`:
- Around line 106-109: The code in IXBase64.h reads kDecodingTable using
static_cast<int>(input[i++]) which on platforms with signed char can produce
negative indices and OOB access; update the indexing to first convert the input
byte to an unsigned type before casting to int (e.g. use
static_cast<int>(static_cast<unsigned char>(input[i++])) or cast to uint8_t then
to int) for the four reads that assign a, b, c, d so bytes with high bit set
index kDecodingTable safely while preserving the existing '=' handling logic.
- Line 52: The code currently obtains a writable pointer via
const_cast<char*>(ret.c_str()) which is undefined behavior; change the pointer
acquisition to use a legal mutable buffer (e.g., use ret.data() in C++17+ or
ensure a mutable std::string copy) so that the pointer `p` points to a
modifiable buffer; update the reference where `p` is used accordingly (look for
`char *p = const_cast<char*>(ret.c_str())`, variable `p`, and the string `ret`
in IXBase64.h and replace with a safe approach such as `char *p = ret.data()` or
create a mutable copy before writing).
- Around line 54-59: The for-loop in the Base64 encoder (the block iterating
with i and using data, in_len, and sEncodingTable) can underflow because it uses
the unsigned expression "in_len - 2"; change the loop guard to a safe check such
as testing in_len before the loop or using an index comparison that cannot
underflow (e.g., use "i + 2 < in_len" or early-return/skip when in_len < 3) so
the body that reads data[i], data[i+1], data[i+2] cannot run on inputs of length
0, 1, or 2.
- Around line 96-101: The code reads input[in_len - 1] and input[in_len - 2]
without checking for empty input, causing OOB when in_len == 0; update the start
of the block in IXBase64.h (variables input, in_len, out_len) to handle empty
input early—e.g., if in_len == 0 return an empty output (or otherwise handle as
a special case) before computing out_len or accessing input[in_len - 1/ -2];
alternatively ensure you only access those indices when in_len >= 2 and adjust
out_len logic accordingly.

In `@ixwebsocket/IXHttpServer.cpp`:
- Around line 123-134: The code builds a file path by concatenating "." + uri
which allows path traversal; sanitize and canonicalize the URI before calling
readAsString. Specifically: strip any query/fragment from request->uri,
percent-decode it, reject or normalize segments like ".." and empty segments (or
collapse them), then join it against a fixed web root (e.g., webRoot +
sanitizedUri) using std::filesystem::path and obtain a canonical/resolved path
(std::filesystem::canonical or equivalent) and verify the resulting path has the
web root as its prefix; only then pass that canonical path to
readAsString(uriPath). Update the logic around variables uri, path, and the call
to readAsString to use the validated canonical path and keep
response_head_file(userAgent()/headers) usage unchanged.

In `@ixwebsocket/IXHttpServer.h`:
- Line 36: The default connection handler set in setDefaultConnectionCallback
currently builds file paths as "." + uri and is vulnerable to path traversal;
update setDefaultConnectionCallback to sanitize and normalize the incoming URI
before constructing the filesystem path: percent-decode and reject or normalize
sequences like "..", collapse multiple slashes, then resolve the resulting path
to an absolute path and verify it is inside your intended document root (e.g.
compare that resolvedPath.starts_with(documentRoot) or use canonicalization)
before calling readAsString/load/std::ifstream; if the check fails return a
403/404 response or serve a safe default. Ensure you perform these checks in the
code path used by setOnConnectionCallback/setDefaultConnectionCallback and reuse
the same sanitization helper for readAsString/load to avoid duplication.

In `@ixwebsocket/IXNetSystem.h`:
- Around line 28-39: The EAGAIN macro is incorrectly mapped to WSATRY_AGAIN;
update the mapping so EAGAIN maps to WSAEWOULDBLOCK (same as EWOULDBLOCK) so
socket non-blocking checks (e.g., the err == EWOULDBLOCK || err == EAGAIN || err
== EINPROGRESS checks in IXSocket.cpp and IXUdpSocket.cpp) work correctly on
Windows; modify the EAGAIN `#define` in the IXNetSystem.h macro block accordingly
and remove or retain WSATRY_AGAIN only for DNS-specific uses if needed.

In `@ixwebsocket/IXSocketServer.cpp`:
- Around line 310-329: The accept loop currently declares client as struct
sockaddr_in and later treats IPv6 connections as if they were sockaddr_in6,
causing invalid memory reads; change the client variable to a generic struct
sockaddr_storage (or declare both sockaddr_in and sockaddr_in6 and cast based on
sa_family) and when handling AF_INET6 cast the accepted address to (struct
sockaddr_in6*) and use its sin6_addr with inet_ntop (use sin_addr for AF_INET),
ensuring addressLen remains socklen_t and all casts and field accesses match the
actual sockaddr type used by accept in the run/accept handling code.

In `@ixwebsocket/IXUuid.cpp`:
- Around line 39-49: The constructor Uuid::Uuid() currently uses static local rd
and dist which causes a data race; make them thread-local instead (declare rd
and dist as thread_local) so each thread has its own std::random_device and
std::uniform_int_distribution instance, then keep the same calls to dist(rd) and
the existing bit-fiddling that sets _ab and _cd; ensure you only change the
storage specifier for rd and dist (or alternatively protect accesses with a
mutex) so uuid generation is safe under concurrent calls to Uuid::Uuid().

In `@ixwebsocket/IXWebSocketMessage.h`:
- Around line 18-56: The WebSocketMessage struct currently stores a const
std::string& (member str) which can dangle; change the member to own the string
(std::string str) and update the constructor(s) to accept an owning parameter
(either std::string by value or std::string&&) and initialize the member with
std::move(s) in the initializer list; remove the deleted rvalue overload since
temporaries will now be safely moved into the owned member. Ensure you update
all references to the constructor signature used by
WebSocketTransport::dispatch() so it passes/moves the string appropriately.

In `@ixwebsocket/IXWebSocketPerMessageDeflate.cpp`:
- Around line 93-96: Add a configurable max-decompressed-size to
WebSocketPerMessageDeflateOptions and enforce it during decompression: extend
WebSocketPerMessageDeflateOptions with a uint64_t maxDecompressedSize (default
reasonable value or 0==unlimited), pass that option into
WebSocketPerMessageDeflateDecompressor, and inside
WebSocketPerMessageDeflateDecompressor::decompress (the loop that calls
inflate() and appends to the output) check the cumulative output size after each
inflate chunk and abort/return false if it exceeds maxDecompressedSize; also
ensure WebSocketPerMessageDeflate::decompress forwards failures and that callers
(e.g., IXWebSocketTransport::emitMessage) treat a false return as a
decompression error.

In `@ixwebsocket/IXWebSocketTransport.cpp`:
- Around line 824-830: The getRandomUnsigned() implementation uses predictable
epoch seconds; replace it with a cryptographically secure RNG in
WebSocketTransport::getRandomUnsigned (e.g., use std::random_device +
std::uniform_int_distribution<unsigned> or call a platform CSPRNG like
getrandom()/BCryptGenRandom()/OpenSSL RAND_bytes() if stronger entropy is
needed), add the appropriate include (<random> or platform headers), and ensure
the returned unsigned is derived from securely generated bytes rather than time.

In `@ixwebsocket/IXWebSocketTransport.h`:
- Line 283: The getRandomUnsigned() implementation currently uses predictable
time-based bits for masking keys; replace its logic with a cryptographically
secure PRNG: initialize a static std::mt19937 (or std::mt19937_64) seeded from
std::random_device and use a std::uniform_int_distribution<uint32_t> to produce
the unsigned masking key in getRandomUnsigned(); ensure the engine is
static/thread-safe (or protected) and that getRandomUnsigned() returns the
uniformly distributed uint32_t value for WebSocket frame masking keys instead of
time-derived values.
🟠 Major comments (41)
ixwebsocket/IXUserAgent.cpp-26-45 (1)

26-45: Reorder platform detection macros: move generic __unix__ check after specific OS checks.

The current code places the __unix__ branch (line 26) before checks for HP-UX, AIX, Apple/Darwin, and Solaris. Since __unix__ is defined on all these systems, the condition at line 26 matches first and prevents subsequent platform-specific branches from executing. On macOS and other systems where __unix__ is defined but BSD is not set in sys/param.h, PLATFORM_NAME remains undefined, causing compilation to fail at line 67 where the macro is used directly.

Move the __unix__ block after the specific OS checks (HP-UX, AIX, Apple/Darwin, Solaris) and add a fallback #define PLATFORM_NAME "unix" when BSD is not defined:

🔧 Suggested reordering + fallback
-#elif defined(__unix__) || !defined(__APPLE__) && defined(__MACH__)
-#include <sys/param.h>
-#if defined(BSD)
-#define PLATFORM_NAME "bsd" // FreeBSD, NetBSD, OpenBSD, DragonFly BSD
-#endif
-#elif defined(__hpux)
-#define PLATFORM_NAME "hp-ux" // HP-UX
-#elif defined(_AIX)
-#define PLATFORM_NAME "aix"                   // IBM AIX
-#elif defined(__APPLE__) && defined(__MACH__) // Apple OSX and iOS (Darwin)
+#elif defined(__hpux)
+#define PLATFORM_NAME "hp-ux" // HP-UX
+#elif defined(_AIX)
+#define PLATFORM_NAME "aix" // IBM AIX
+#elif defined(__sun) && defined(__SVR4)
+#define PLATFORM_NAME "solaris" // Oracle Solaris, Open Indiana
+#elif defined(__APPLE__) && defined(__MACH__) // Apple OSX and iOS (Darwin)
 `#include` <TargetConditionals.h>
 `#if` TARGET_IPHONE_SIMULATOR == 1
 `#define` PLATFORM_NAME "ios" // Apple iOS
 `#elif` TARGET_OS_IPHONE == 1
 `#define` PLATFORM_NAME "ios" // Apple iOS
 `#elif` TARGET_OS_MAC == 1
 `#define` PLATFORM_NAME "macos" // Apple OSX
 `#endif`
-#elif defined(__sun) && defined(__SVR4)
-#define PLATFORM_NAME "solaris" // Oracle Solaris, Open Indiana
+#elif defined(__unix__) || (!defined(__APPLE__) && defined(__MACH__))
+#include <sys/param.h>
+#if defined(BSD)
+#define PLATFORM_NAME "bsd" // FreeBSD, NetBSD, OpenBSD, DragonFly BSD
+#else
+#define PLATFORM_NAME "unix"
+#endif
ixwebsocket/IXUniquePtr.h-9-16 (1)

9-16: Add explicit <utility> include for std::forward portability.
Line 16 uses std::forward, which requires <utility> per the C++ standard. While this may compile on many platforms due to transitive includes through <memory>, explicitly including <utility> ensures portability across different standard library implementations.

Proposed fix
 `#include` <memory>
+#include <utility>
ixwebsocket/IXGzipCodec.cpp-74-118 (1)

74-118: Decompression has no output size limit — potential decompression bomb DoS.

The gzipDecompress function will decompress input indefinitely until inflate returns Z_STREAM_END. A maliciously crafted small compressed payload can expand to gigabytes of output, exhausting memory.

Consider adding a maximum output size parameter or internal limit to protect against this attack vector, especially since this is a networking library that may process untrusted input.

Do you want me to generate a modified implementation with a configurable maximum decompression size?

ixwebsocket/IXSelectInterruptPipe.cpp-64-86 (1)

64-86: Resource leak: pipe descriptors not closed on fcntl failure.

When fcntl fails to set O_NONBLOCK, the code sets _fildes to -1 but does not close the successfully created pipe descriptors from line 55. This leaks two file descriptors per failed initialization.

🔧 Proposed fix to close pipe on fcntl failure
         if (fcntl(_fildes[kPipeReadIndex], F_SETFL, O_NONBLOCK) == -1)
         {
             std::stringstream ss;
             ss << "SelectInterruptPipe::init() failed in fcntl(..., O_NONBLOCK) call"
                << " : " << strerror(errno);
             errorMsg = ss.str();
 
+            ::close(_fildes[kPipeReadIndex]);
+            ::close(_fildes[kPipeWriteIndex]);
             _fildes[kPipeReadIndex] = -1;
             _fildes[kPipeWriteIndex] = -1;
             return false;
         }
 
         if (fcntl(_fildes[kPipeWriteIndex], F_SETFL, O_NONBLOCK) == -1)
         {
             std::stringstream ss;
             ss << "SelectInterruptPipe::init() failed in fcntl(..., O_NONBLOCK) call"
                << " : " << strerror(errno);
             errorMsg = ss.str();
 
+            ::close(_fildes[kPipeReadIndex]);
+            ::close(_fildes[kPipeWriteIndex]);
             _fildes[kPipeReadIndex] = -1;
             _fildes[kPipeWriteIndex] = -1;
             return false;
         }
ixwebsocket/IXGzipCodec.cpp-31-36 (1)

31-36: deflateInit2 return value not checked — undefined behavior on failure.

If deflateInit2 fails (e.g., due to memory allocation failure), the z_stream remains uninitialized and subsequent calls to deflate() and deflateEnd() will operate on invalid state, causing undefined behavior or crashes.

🔧 Proposed fix to check initialization
-        deflateInit2(&zs,
+        int initResult = deflateInit2(&zs,
                      Z_DEFAULT_COMPRESSION,
                      Z_DEFLATED,
                      windowBits | GZIP_ENCODING,
                      8,
                      Z_DEFAULT_STRATEGY);
+
+        if (initResult != Z_OK)
+        {
+            return std::string();
+        }
 
         zs.next_in = (Bytef*) str.data();
ixwebsocket/IXGetFreePort.h-9-12 (1)

9-12: Infinite loop risk and unreachable code in getFreePort() implementation.

The getFreePort() function in IXGetFreePort.cpp (lines 71–91) contains a while(true) loop that only exits when port > 1024. Since getAnyFreePort() binds to port 0 to request an OS-assigned ephemeral port, it depends on the system's ephemeral port range configuration. On systems where this range includes ports ≤ 1024, the loop would run indefinitely—a potential DoS vector. While uncommon, this is possible on misconfigured systems.

Additionally, the return -1; statement at line 91 is unreachable dead code, as the loop either returns inside the condition or runs indefinitely.

ixwebsocket/IXHttp.cpp-132-173 (1)

132-173: Add a hard cap on Content-Length to prevent memory exhaustion.

readBytes(contentLength, …) can allocate/read arbitrarily large bodies under attacker control. This is a classic DoS vector; enforce a maximum (configurable) size before reading.

🛡️ Suggested guard
             if (contentLength < 0)
             {
                 return std::make_tuple(
                     false, "Error: 'Content-Length' should be a positive integer", httpRequest);
             }
+            constexpr int kMaxBodySize = 10 * 1024 * 1024; // 10MB (tune/configure)
+            if (contentLength > kMaxBodySize)
+            {
+                return std::make_tuple(
+                    false, "Error: 'Content-Length' exceeds maximum allowed size", httpRequest);
+            }
 
             auto res = socket->readBytes(contentLength, nullptr, nullptr, isCancellationRequested);
ixwebsocket/IXWebSocketHttpHeaders.cpp-23-67 (1)

23-67: Reject overlong/unterminated header lines to avoid OOB/underflow.

If a header line exceeds 1023 bytes or lacks CRLF, the current code can (a) silently truncate and (b) access out of bounds or underflow when computing substr. Fail fast when the line doesn’t terminate properly and guard start/length before indexing.

🔧 Proposed hardening
             for (i = 0; i < 2 || (i < 1023 && line[i - 2] != '\r' && line[i - 1] != '\n'); ++i)
             {
                 if (!socket->readByte(line + i, isCancellationRequested))
                 {
                     return std::make_pair(false, headers);
                 }
 
                 if (line[i] == ':' && colon == 0)
                 {
                     colon = i;
                 }
             }
+            if (i >= 1023 && (line[i - 2] != '\r' || line[i - 1] != '\n'))
+            {
+                // header line too long or unterminated
+                return std::make_pair(false, headers);
+            }
             if (line[0] == '\r' && line[1] == '\n')
             {
                 break;
             }
@@
             if (colon > 0)
             {
                 line[i] = '\0';
                 std::string lineStr(line);
+                if (lineStr.size() < 2 || static_cast<size_t>(colon + 1) >= lineStr.size())
+                {
+                    return std::make_pair(false, headers);
+                }
                 int start = colon + 1;
-                while (lineStr[start] == ' ')
+                while (start < static_cast<int>(lineStr.size()) && lineStr[start] == ' ')
                 {
                     start++;
                 }
+                if (lineStr.size() < static_cast<size_t>(start + 2))
+                {
+                    return std::make_pair(false, headers);
+                }
 
                 std::string name(lineStr.substr(0, colon));
                 std::string value(lineStr.substr(start, lineStr.size() - start - 2));
ixwebsocket/IXSocketFactory.cpp-43-55 (1)

43-55: Missing error message when TLS is enabled but no backend is matched.

When IXWEBSOCKET_USE_TLS is defined but none of the specific backend macros (IXWEBSOCKET_USE_MBED_TLS, IXWEBSOCKET_USE_OPEN_SSL, __APPLE__) are defined, the socket pointer remains nullptr and the function proceeds to call socket->init() at line 57, causing a null pointer dereference.

🐛 Proposed fix
 `#ifdef` IXWEBSOCKET_USE_TLS
 `#if` defined(IXWEBSOCKET_USE_MBED_TLS)
             socket = ix::make_unique<SocketMbedTLS>(tlsOptions, fd);
 `#elif` defined(IXWEBSOCKET_USE_OPEN_SSL)
             socket = ix::make_unique<SocketOpenSSL>(tlsOptions, fd);
 `#elif` defined(__APPLE__)
             socket = ix::make_unique<SocketAppleSSL>(tlsOptions, fd);
+#else
+            errorMsg = "TLS is enabled but no TLS backend was configured.";
+            return nullptr;
 `#endif`
 `#else`
             errorMsg = "TLS support is not enabled on this platform.";
             return nullptr;
 `#endif`
ixwebsocket/IXSocketConnect.cpp-59-93 (1)

59-93: Performance issue: SelectInterruptPtr created repeatedly inside polling loop.

A new SelectInterruptPtr is allocated via createSelectInterrupt() on every iteration of the polling loop (line 70). This is inefficient and creates unnecessary allocation overhead. Move the creation outside the loop.

Proposed fix
+        SelectInterruptPtr selectInterrupt = ix::createSelectInterrupt();
         for (;;)
         {
             if (isCancellationRequested && isCancellationRequested()) // Must handle timeout as well
             {
                 Socket::closeSocket(fd);
                 errMsg = "Cancelled";
                 return -1;
             }

             int timeoutMs = 10;
             bool readyToRead = false;
-            SelectInterruptPtr selectInterrupt = ix::createSelectInterrupt();
             PollResultType pollResult = Socket::poll(readyToRead, timeoutMs, fd, selectInterrupt);
ixwebsocket/IXWebSocketPerMessageDeflate.cpp-71-81 (1)

71-81: The window bits parameters are swapped for server-side usage, violating RFC 7692.

The init() method uses getClientMaxWindowBits() for compression and getServerMaxWindowBits() for decompression, which is correct only for client-side. Since the same WebSocketPerMessageDeflate class is used for both client (line 129 in IXWebSocketTransport.cpp) and server (line 184 in IXWebSocketTransport.cpp) without role differentiation, the parameters are reversed when used server-side:

  • Client-side: compressor should use client_max_window_bits ✓, decompressor should use server_max_window_bits ✓
  • Server-side: compressor should use server_max_window_bits ✗ (currently uses client), decompressor should use client_max_window_bits ✗ (currently uses server)

This violates RFC 7692 and will cause interoperability issues when negotiating non-default window sizes. The class needs either role-aware parameter swapping or separate implementations for client and server.

ixwebsocket/IXWebSocketProxyServer.cpp-107-112 (1)

107-112: Add timeout/exit conditions to the remote‑connect wait loop.

The loop waits until ReadyState::Open with no timeout (Line 109), so a failed backend connect can hang the handler indefinitely and tie up resources.

⏱️ Suggested fix with timeout
-                            while (state->webSocket().getReadyState() != ReadyState::Open)
-                            {
-                                std::this_thread::sleep_for(std::chrono::milliseconds(10));
-                            }
+                            auto start = std::chrono::steady_clock::now();
+                            const auto kConnectTimeout = std::chrono::seconds(10);
+                            while (state->webSocket().getReadyState() != ReadyState::Open)
+                            {
+                                if (std::chrono::steady_clock::now() - start > kConnectTimeout)
+                                {
+                                    state->setTerminated();
+                                    return;
+                                }
+                                std::this_thread::sleep_for(std::chrono::milliseconds(10));
+                            }
ixwebsocket/IXWebSocketServer.cpp-190-215 (1)

190-215: Unbounded flush loop can stall broadcast thread.

The loop waits until bufferedAmount() reaches zero with repeated sleeps; a slow or stalled client can block message processing indefinitely (DoS risk). Add a maximum wait or drop/close slow clients.

⏳ Suggested bounded flush
-                            do
-                            {
-                                std::chrono::duration<double, std::milli> duration(500);
-                                std::this_thread::sleep_for(duration);
-                            } while (client->bufferedAmount() != 0);
+                            auto start = std::chrono::steady_clock::now();
+                            const auto kMaxFlushWait = std::chrono::seconds(5);
+                            while (client->bufferedAmount() != 0 &&
+                                   std::chrono::steady_clock::now() - start < kMaxFlushWait)
+                            {
+                                std::this_thread::sleep_for(std::chrono::milliseconds(50));
+                            }
+                            if (client->bufferedAmount() != 0)
+                            {
+                                client->close();
+                            }
ixwebsocket/IXDNSLookup.h-48-49 (1)

48-49: Thread safety issue: getErrMsg() returns reference to protected data.

Based on the implementation in IXDNSLookup.cpp (lines 183-187), getErrMsg() acquires a lock, then returns a const std::string& to _errMsg. Once the function returns, the lock is released, but the caller still holds a reference to _errMsg. If another thread calls setErrMsg(), this reference becomes a data race.

The signature should return by value:

-        const std::string& getErrMsg();
+        std::string getErrMsg();

And the implementation should copy:

std::string DNSLookup::getErrMsg()
{
    std::lock_guard<std::mutex> lock(_errMsgMutex);
    return _errMsg; // return copy
}
ixwebsocket/IXConnectionState.cpp-13-17 (1)

13-17: Member _remotePort is not initialized.

The constructor initializes _terminated but leaves _remotePort uninitialized. Reading an uninitialized int in getRemotePort() before setRemotePort() is called results in undefined behavior.

Proposed fix
     ConnectionState::ConnectionState()
         : _terminated(false)
+        , _remotePort(0)
     {
         computeId();
     }
ixwebsocket/IXConnectionState.h-49-50 (1)

49-50: Member _remotePort should have a default initializer.

To prevent undefined behavior from reading uninitialized memory, _remotePort should be initialized in the class declaration or the constructor initializer list.

Proposed fix in header
         std::string _remoteIp;
-        int _remotePort;
+        int _remotePort = 0;
ixwebsocket/IXUdpSocket.cpp-26-32 (1)

26-32: Race condition in close() method.

The check if (_sockfd == -1) and subsequent closeSocket(_sockfd); _sockfd = -1; is not atomic. If two threads call close() simultaneously, both could pass the check before either sets _sockfd = -1, leading to a double-close. Since _sockfd is std::atomic<int>, use exchange for atomicity.

Proposed fix using atomic exchange
     void UdpSocket::close()
     {
-        if (_sockfd == -1) return;
-
-        closeSocket(_sockfd);
-        _sockfd = -1;
+        int fd = _sockfd.exchange(-1);
+        if (fd == -1) return;
+
+        closeSocket(fd);
     }
ixwebsocket/IXNetSystem.cpp-220-240 (1)

220-240: Missing initialization check for fd->fd before use with FD_SET.

Unlike the event-based path (line 129), this select-based fallback does not check if fd->fd >= 0 before using it with FD_SET. Negative file descriptors could cause undefined behavior.

Proposed fix
             for (nfds_t i = 0; i < nfds; ++i)
             {
                 struct pollfd* fd = &fds[i];
 
+                if (fd->fd < 0)
+                {
+                    continue;
+                }
                 if (fd->fd > maxfd)
                 {
                     maxfd = fd->fd;
                 }
ixwebsocket/IXUdpSocket.cpp-94-101 (1)

94-101: Incorrect error message source after getaddrinfo failure.

When getaddrinfo fails, the error message should use gai_strerror(ret) (the GAI-specific error function), not strerror(UdpSocket::getErrno()). The errno/WSAGetLastError is not set by getaddrinfo; instead, it returns its own error codes.

Proposed fix
         int ret = getaddrinfo(host.c_str(), nullptr, &hints, &result);
         if (ret != 0)
         {
-            errMsg = strerror(UdpSocket::getErrno());
-            freeaddrinfo(result);
+            errMsg = gai_strerror(ret);
+            // result may be nullptr on failure, but freeaddrinfo handles nullptr safely
             close();
             return false;
         }

Note: Also remove the freeaddrinfo(result) call on the error path since result may be undefined/null when getaddrinfo fails.

ixwebsocket/IXHttpServer.h-38-40 (1)

38-40: makeDebugServer() logs full request headers and body without sensitive data filtering.

The implementation (lines 205-237 in IXHttpServer.cpp) logs all HTTP headers and the complete request body via logInfo(). This publicly exposed method could inadvertently log sensitive information (authentication tokens, PII, credentials) if called by library users.

Add a documentation warning that makeDebugServer() is strictly for development/testing, or filter sensitive headers (Authorization, Cookie, etc.) before logging.

ixwebsocket/IXWebSocketHandshake.cpp-329-336 (1)

329-336: Inconsistent case handling in Upgrade header validation.

Line 329 uses insensitiveStringCompare for the lowercase key headers["upgrade"], but line 330 checks the original case headers["Upgrade"]. Since the header parsing likely normalizes keys to lowercase, headers["Upgrade"] would not match and this condition would never be true for the Firefox special case.

🔧 Proposed fix
-        if (!insensitiveStringCompare(headers["upgrade"], "WebSocket") &&
-            headers["Upgrade"] != "keep-alive, Upgrade") // special case for firefox
+        if (!insensitiveStringCompare(headers["upgrade"], "WebSocket") &&
+            headers["upgrade"] != "keep-alive, Upgrade") // special case for firefox

Note: The Firefox special case value should also use case-insensitive comparison for robustness.

ixwebsocket/IXSocketTLSOptions.cpp-34-39 (1)

34-39: In-memory CA validation incorrectly attempts file existence check.

When caFile contains an in-memory certificate (starting with "-----BEGIN CERTIFICATE-----"), this code attempts to open it as a file path, which will fail. The check should also exclude isUsingInMemoryCAs() case.

🔧 Proposed fix
-            if (!caFile.empty() && caFile != kTLSCAFileDisableVerify &&
-                caFile != kTLSCAFileUseSystemDefaults && !std::ifstream(caFile))
+            if (!caFile.empty() && caFile != kTLSCAFileDisableVerify &&
+                caFile != kTLSCAFileUseSystemDefaults && 
+                caFile.find(kTLSInMemoryMarker) == std::string::npos &&
+                !std::ifstream(caFile))
             {
                 _errMsg = "caFile not found: " + caFile;
                 return false;
             }
ixwebsocket/IXHttpServer.cpp-94-95 (1)

94-95: Unhandled HTTP parse errors can lead to unexpected behavior.

The FIXME comment indicates that errors from Http::parseRequest are not handled. If parsing fails (e.g., malformed request), the code may proceed with invalid or uninitialized request data, potentially causing undefined behavior or security issues.

🛡️ Proposed fix
         auto ret = Http::parseRequest(socket, _timeoutSecs);
-        // FIXME: handle errors in parseRequest
 
-        if (std::get<0>(ret))
+        if (!std::get<0>(ret))
+        {
+            // Parsing failed - log and close connection
+            logError("Failed to parse HTTP request: " + std::get<1>(ret));
+            connectionState->setTerminated();
+            return;
+        }
+
         {
             auto request = std::get<2>(ret);
ixwebsocket/IXSocketMbedTLS.cpp-354-378 (1)

354-378: Unreachable code in recv() - while(true) loop always exits on first iteration.

The while (true) loop is misleading because every code path within the loop ends with return. The loop never actually iterates more than once. This appears to be leftover from a refactor or a bug.

If the intent was to retry on WANT_READ/WANT_WRITE, the current implementation doesn't do that - it returns -1 with EWOULDBLOCK instead of retrying.

🔧 Proposed fix - remove misleading loop
     ssize_t SocketMbedTLS::recv(void* buf, size_t nbyte)
     {
-        while (true)
-        {
-            std::lock_guard<std::mutex> lock(_mutex);
+        std::lock_guard<std::mutex> lock(_mutex);
 
-            ssize_t res = mbedtls_ssl_read(&_ssl, (unsigned char*) buf, (int) nbyte);
+        ssize_t res = mbedtls_ssl_read(&_ssl, (unsigned char*) buf, nbyte);
 
-            if (res > 0)
-            {
-                return res;
-            }
+        if (res > 0)
+        {
+            return res;
+        }
 
-            if (res == 0)
-            {
-                errno = ECONNRESET;
-            }
+        if (res == 0)
+        {
+            errno = ECONNRESET;
+        }
+        else if (res == MBEDTLS_ERR_SSL_WANT_READ || res == MBEDTLS_ERR_SSL_WANT_WRITE)
+        {
+            errno = EWOULDBLOCK;
+        }
 
-            if (res == MBEDTLS_ERR_SSL_WANT_READ || res == MBEDTLS_ERR_SSL_WANT_WRITE)
-            {
-                errno = EWOULDBLOCK;
-            }
-            return -1;
-        }
+        return -1;
     }
ixwebsocket/IXDNSLookup.cpp-184-188 (1)

184-188: Race condition: returning reference to mutex-protected member after lock release.

getErrMsg() returns a reference to _errMsg after the lock guard goes out of scope. The caller can read stale or torn data if another thread modifies _errMsg concurrently.

🔒 Proposed fix: return by value
-    const std::string& DNSLookup::getErrMsg()
+    std::string DNSLookup::getErrMsg()
     {
         std::lock_guard<std::mutex> lock(_errMsgMutex);
         return _errMsg;
     }

Note: The declaration in IXDNSLookup.h must also be updated to return std::string instead of const std::string&.

ixwebsocket/IXSocketOpenSSL.cpp-464-488 (1)

464-488: Security issue: Certificate/key loading failures don't abort the TLS setup.

When hasCertAndKey() is true but loading fails (Lines 466-487), the function sets errMsg but continues execution instead of returning false. This means the TLS connection may proceed without the intended client certificate, potentially failing mutual TLS silently or connecting with weaker authentication than expected.

🔒 Proposed fix: return false on cert/key errors
         if (_tlsOptions.hasCertAndKey())
         {
             if (SSL_CTX_use_certificate_chain_file(_ssl_context, _tlsOptions.certFile.c_str()) != 1)
             {
                 auto sslErr = ERR_get_error();
                 errMsg = "OpenSSL failed - SSL_CTX_use_certificate_chain_file(\"" +
                          _tlsOptions.certFile + "\") failed: ";
                 errMsg += ERR_error_string(sslErr, nullptr);
+                return false;
             }
             else if (SSL_CTX_use_PrivateKey_file(
                          _ssl_context, _tlsOptions.keyFile.c_str(), SSL_FILETYPE_PEM) != 1)
             {
                 auto sslErr = ERR_get_error();
                 errMsg = "OpenSSL failed - SSL_CTX_use_PrivateKey_file(\"" + _tlsOptions.keyFile +
                          "\") failed: ";
                 errMsg += ERR_error_string(sslErr, nullptr);
+                return false;
             }
             else if (!SSL_CTX_check_private_key(_ssl_context))
             {
                 auto sslErr = ERR_get_error();
                 errMsg = "OpenSSL failed - cert/key mismatch(\"" + _tlsOptions.certFile + ", " +
                          _tlsOptions.keyFile + "\")";
                 errMsg += ERR_error_string(sslErr, nullptr);
+                return false;
             }
         }
ixwebsocket/IXSocketOpenSSL.cpp-426-459 (1)

426-459: Potential DoS: Server TLS handshake loop lacks timeout/cancellation.

openSSLServerHandshake loops indefinitely on SSL_ERROR_WANT_READ/SSL_ERROR_WANT_WRITE without any timeout or cancellation mechanism. A malicious client could keep the server stuck in this loop by sending partial TLS handshake data, consuming server resources.

In contrast, openSSLClientHandshake (Line 389) includes a cancellation check.

🛡️ Proposed fix: add timeout/cancellation to server handshake
-    bool SocketOpenSSL::openSSLServerHandshake(std::string& errMsg)
+    bool SocketOpenSSL::openSSLServerHandshake(std::string& errMsg,
+                                               const CancellationRequest& isCancellationRequested)
     {
         while (true)
         {
             if (_ssl_connection == nullptr || _ssl_context == nullptr)
             {
                 return false;
             }

+            if (isCancellationRequested && isCancellationRequested())
+            {
+                errMsg = "Cancellation requested";
+                return false;
+            }
+
             ERR_clear_error();
             int accept_result = SSL_accept(_ssl_connection);

The caller would need to provide a cancellation request with timeout.

ixwebsocket/IXDNSLookup.cpp-70-78 (1)

70-78: Potential undefined behavior: freeaddrinfo(nullptr) when lookup fails.

When getaddrinfo fails, res is set to nullptr (Line 75), but the returned AddrInfoPtr still has freeaddrinfo as its deleter. When this shared_ptr is destroyed, it will call freeaddrinfo(nullptr), which is undefined behavior on some platforms (POSIX doesn't guarantee it's safe).

🐛 Proposed fix: use custom deleter that checks for null
     struct addrinfo* res;
     int getaddrinfo_result = getaddrinfo(hostname.c_str(), sport.c_str(), &hints, &res);
     if (getaddrinfo_result)
     {
         errMsg = gai_strerror(getaddrinfo_result);
-        res = nullptr;
+        return nullptr;
     }
-    return AddrInfoPtr{ res, freeaddrinfo };
+    return AddrInfoPtr(res, freeaddrinfo);

Alternatively, use a custom deleter:

return AddrInfoPtr(res, [](addrinfo* p) { if (p) freeaddrinfo(p); });
ixwebsocket/IXWebSocketHandshake.h-44-46 (1)

44-46: Use a cryptographically secure PRNG for WebSocket Sec-WebSocket-Key generation.

The genRandomString implementation uses std::random_device + std::default_random_engine, which is not guaranteed to be cryptographically secure and may fall back to a deterministic PRNG on some platforms. RFC 6455 requires the 16-byte WebSocket key to be cryptographically unpredictable. Additionally, the 26-character alphabet ("0123456789ABCDEFGHabcdefgh") provides only ~75 bits of entropy instead of the required ~128 bits. Replace with a platform CSPRNG API (e.g., BCryptGenRandom on Windows, /dev/urandom on POSIX) or a cryptographic library like OpenSSL.

ixwebsocket/IXSocketServer.cpp-387-395 (1)

387-395: Null pointer dereference if connection state factory is not set.

The static analysis correctly identifies that connectionState may be null if _connectionStateFactory is set but returns nullptr, or if _connectionStateFactory is falsy and the block is skipped entirely. Lines 392-394 unconditionally dereference connectionState.

🐛 Proposed fix
             std::shared_ptr<ConnectionState> connectionState;
             if (_connectionStateFactory)
             {
                 connectionState = _connectionStateFactory();
             }
+
+            if (!connectionState)
+            {
+                logError("SocketServer::run() failed to create connection state");
+                Socket::closeSocket(clientFd);
+                continue;
+            }
+
             connectionState->setOnSetTerminatedCallback([this] { onSetTerminatedCallback(); });
             connectionState->setRemoteIp(remoteIp);
             connectionState->setRemotePort(remotePort);
ixwebsocket/IXWebSocketPerMessageDeflateCodec.cpp-209-251 (1)

209-251: Missing output size limit in decompress - potential decompression bomb vulnerability.

The decompress function has no upper bound on the decompressed output size. A malicious peer could send a small compressed payload that expands to gigabytes, causing memory exhaustion (zip bomb attack).

Consider adding a maximum output size limit.

🛡️ Proposed mitigation
-    bool WebSocketPerMessageDeflateDecompressor::decompress(const std::string& in, std::string& out)
+    bool WebSocketPerMessageDeflateDecompressor::decompress(const std::string& in, std::string& out, size_t maxOutputSize)
     {
 `#ifdef` IXWEBSOCKET_USE_ZLIB
+        const size_t defaultMaxOutputSize = 100 * 1024 * 1024; // 100MB default limit
+        if (maxOutputSize == 0) maxOutputSize = defaultMaxOutputSize;
+
         // ... existing setup code ...
 
         do
         {
             _inflateState.avail_out = (uInt) _compressBuffer.size();
             _inflateState.next_out = &_compressBuffer.front();
 
             int ret = inflate(&_inflateState, Z_SYNC_FLUSH);
 
             if (ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR)
             {
                 return false; // zlib error
             }
 
             out.append(reinterpret_cast<char*>(&_compressBuffer.front()),
                        _compressBuffer.size() - _inflateState.avail_out);
+
+            if (out.size() > maxOutputSize)
+            {
+                return false; // Decompression bomb protection
+            }
         } while (_inflateState.avail_out == 0);
ixwebsocket/IXWebSocket.cpp-27-27 (1)

27-27: Static callback without synchronization poses data race risk.

The static _onTrafficTrackerCallback is accessed from multiple threads (via invokeTrafficTrackerCallback) but setTrafficTrackerCallback writes to it without synchronization. This is a data race (undefined behavior in C++).

🔒 Proposed fix using atomic or mutex

Consider using std::atomic<OnTrafficTrackerCallback*> or protecting access with a static mutex. Alternatively, if the callback is only set once during initialization before any threads are started, document this constraint clearly.

+    static std::mutex _trafficTrackerMutex;
     static OnTrafficTrackerCallback WebSocket::_onTrafficTrackerCallback = nullptr;

And in setTrafficTrackerCallback and invokeTrafficTrackerCallback:

 void WebSocket::setTrafficTrackerCallback(const OnTrafficTrackerCallback& callback)
 {
+    std::lock_guard<std::mutex> lock(_trafficTrackerMutex);
     _onTrafficTrackerCallback = callback;
 }

 void WebSocket::invokeTrafficTrackerCallback(size_t size, bool incoming)
 {
+    OnTrafficTrackerCallback callback;
+    {
+        std::lock_guard<std::mutex> lock(_trafficTrackerMutex);
+        callback = _onTrafficTrackerCallback;
+    }
+    if (callback)
     {
-        _onTrafficTrackerCallback(size, incoming);
+        callback(size, incoming);
     }
 }
ixwebsocket/IXHttpClient.cpp-193-216 (1)

193-216: Potential HTTP header injection via extraHeaders.

User-provided header values from args->extraHeaders (Line 194-197) are written directly into the HTTP request without sanitization. If a header value contains \r\n, an attacker could inject arbitrary headers or even split the request.

🛡️ Proposed mitigation

Validate that header names and values don't contain CR/LF characters:

 // Append extra headers
 for (auto&& it : args->extraHeaders)
 {
+    // Reject headers with CR/LF to prevent header injection
+    if (it.first.find_first_of("\r\n") != std::string::npos ||
+        it.second.find_first_of("\r\n") != std::string::npos)
+    {
+        return std::make_shared<HttpResponse>(code,
+                                              description,
+                                              HttpErrorCode::Invalid,
+                                              headers,
+                                              payload,
+                                              "Invalid header: contains CR/LF",
+                                              uploadSize,
+                                              downloadSize);
+    }
     ss << it.first << ": " << it.second << "\r\n";
 }
ixwebsocket/IXHttpClient.cpp-415-444 (1)

415-444: Potential memory exhaustion from unbounded Content-Length.

The Content-Length header is parsed and used directly to reserve payload memory (Line 442). A malicious server could send a huge Content-Length value, causing the client to attempt allocating massive amounts of memory before receiving any data.

Consider adding a configurable maximum response size limit.

🛡️ Proposed mitigation
+// Add to HttpRequestArgs or HttpClient
+static const size_t kDefaultMaxResponseSize = 100 * 1024 * 1024; // 100MB
+size_t maxResponseSize = kDefaultMaxResponseSize;

 // In the Content-Length handling:
 ss >> contentLength;
+if (contentLength > args->maxResponseSize) {
+    return std::make_shared<HttpResponse>(code,
+                                          description,
+                                          HttpErrorCode::ResponseTooLarge,
+                                          headers,
+                                          payload,
+                                          "Response exceeds maximum allowed size",
+                                          uploadSize,
+                                          downloadSize);
+}
ixwebsocket/IXWebSocket.cpp-629-633 (1)

629-633: Returning reference to internal vector while holding lock causes use-after-unlock.

getSubProtocols() returns a reference to _subProtocols while holding _configMutex, but the lock is released when the function returns. Any caller using the returned reference after the function returns is accessing the vector without synchronization, which is a data race if another thread calls addSubProtocol() concurrently.

🔒 Proposed fix: return by value
-const std::vector<std::string>& WebSocket::getSubProtocols()
+std::vector<std::string> WebSocket::getSubProtocols() const
 {
     std::lock_guard<std::mutex> lock(_configMutex);
     return _subProtocols;
 }

This requires updating the header declaration accordingly.

ixwebsocket/IXHttpClient.cpp-446-524 (1)

446-524: Chunked transfer encoding lacks total size limit - DoS vector.

The chunked transfer decoding loop (Lines 451-523) accumulates chunks indefinitely without any upper bound on total payload size. A malicious server could send an endless stream of chunks, exhausting client memory.

🛡️ Proposed mitigation

Track total bytes received and enforce a limit:

+size_t totalBytesReceived = 0;
 while (true)
 {
     // ... read chunk size ...
+    totalBytesReceived += chunkSize;
+    if (totalBytesReceived > args->maxResponseSize) {
+        return std::make_shared<HttpResponse>(code,
+                                              description,
+                                              HttpErrorCode::ResponseTooLarge,
+                                              headers,
+                                              payload,
+                                              "Chunked response exceeds maximum allowed size",
+                                              uploadSize,
+                                              downloadSize);
+    }
     // ... read chunk ...
ixwebsocket/IXHttpClient.cpp-367-400 (1)

367-400: SSRF risk: Redirect handling follows arbitrary Location headers without validation.

When following redirects (lines 367-400), the code extracts the Location header directly and passes it to request() without any validation. An attacker controlling a redirect response could redirect requests to internal services (e.g., http://169.254.169.254/ for cloud metadata, or http://localhost:8080/admin).

The HttpRequestArgs structure offers only followRedirects (bool) and maxRedirects (int) options—no mechanism to validate or restrict redirect destinations.

Consider:

  1. Allowing only same-origin redirects by default
  2. Providing a callback to validate redirect URLs
  3. Blocking redirects to private IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x, link-local IPv6)
ixwebsocket/IXWebSocketTransport.cpp-342-346 (1)

342-346: Data race: _lastSendPingTimePoint accessed without mutex.

Line 344 reads _lastSendPingTimePoint directly, but other code paths (Lines 246, 288, 298, 1044) access it while holding _lastSendPingTimePointMutex. This is a data race that could cause undefined behavior.

🔒 Proposed fix to use mutex-protected access
         if (_pingIntervalSecs > 0)
         {
             // compute lasting delay to wait for next ping / timeout, if at least one set
             auto now = std::chrono::steady_clock::now();
+            std::chrono::steady_clock::time_point lastPing;
+            {
+                std::lock_guard<std::mutex> lock(_lastSendPingTimePointMutex);
+                lastPing = _lastSendPingTimePoint;
+            }
             int timeSinceLastPingMs = (int) std::chrono::duration_cast<std::chrono::milliseconds>(
-                                          now - _lastSendPingTimePoint)
+                                          now - lastPing)
                                           .count();
             lastingTimeoutDelayInMs = (1000 * _pingIntervalSecs) - timeSinceLastPingMs;
         }
ixwebsocket/IXWebSocketTransport.cpp-117-168 (1)

117-168: Redirect loop follows Location header without URL validation.

The redirect handling at Lines 145-158 follows the Location header without validating the scheme. A malicious server could redirect from wss:// to http:// or to internal addresses (SSRF). Consider:

  1. Validating that the redirect target uses ws:// or wss:// scheme only
  2. Optionally restricting redirects to the same origin or allowing explicit opt-in for cross-origin redirects
🛡️ Proposed fix to validate redirect scheme
                 remoteUrl = it->second;
+                
+                // Validate redirect URL scheme to prevent protocol downgrade or SSRF
+                std::string redirectProtocol, redirectHost, redirectPath, redirectQuery;
+                int redirectPort;
+                if (!UrlParser::parse(remoteUrl, redirectProtocol, redirectHost, redirectPath, redirectQuery, redirectPort))
+                {
+                    return WebSocketInitResult(false, 0, "Invalid redirect URL");
+                }
+                if (redirectProtocol != "ws" && redirectProtocol != "wss")
+                {
+                    return WebSocketInitResult(false, 0, "Redirect to non-WebSocket protocol rejected");
+                }
+                // Optional: reject downgrade from wss to ws
+                if (tls && redirectProtocol == "ws")
+                {
+                    return WebSocketInitResult(false, 0, "TLS downgrade via redirect rejected");
+                }
+                
                 continue;
ixwebsocket/IXWebSocketTransport.cpp-1275-1279 (1)

1275-1279: Race condition: Returning reference to mutex-protected member.

getCloseReason() returns a const std::string& but releases the mutex before the caller uses the reference. Another thread could call setCloseReason() and modify _closeReason while the caller is still using the reference.

🔒 Proposed fix to return by value
-    const std::string& WebSocketTransport::getCloseReason() const
+    std::string WebSocketTransport::getCloseReason() const
     {
         std::lock_guard<std::mutex> lock(_closeReasonMutex);
         return _closeReason;
     }

Also update the declaration in the header file to return std::string instead of const std::string&.

ixwebsocket/IXWebSocketTransport.cpp-541-552 (1)

541-552: Add payload size validation to prevent memory exhaustion DoS.

The maxFrameSize check of 1ULL << 63 is ineffective—all valid frame sizes pass through. A malicious peer can send a frame header claiming a multi-gigabyte payload, causing the receiver to set _rxbufWanted to an enormous value and buffer indefinitely via _rxbuf.insert() (line 1133), potentially exhausting memory.

Add a configurable maximum payload size limit and validate frame size before setting _rxbufWanted. Close the connection when a frame exceeds the limit.

size_t out_len = 4 * ((in_len + 2) / 3);
std::string ret(out_len, '\0');
size_t i;
char *p = const_cast<char*>(ret.c_str());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Undefined behavior: modifying string through c_str().

const_cast<char*>(ret.c_str()) and then writing through p is undefined behavior. The c_str() method returns a pointer to a read-only buffer, and modifying it violates the C++ standard.

🐛 Proposed fix using legal mutable access
-    char *p = const_cast<char*>(ret.c_str());
+    char *p = &ret[0];  // Legal mutable access in C++11 and later

Alternatively in C++17+: char *p = ret.data();

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
char *p = const_cast<char*>(ret.c_str());
char *p = &ret[0]; // Legal mutable access in C++11 and later
🤖 Prompt for AI Agents
In `@ixwebsocket/IXBase64.h` at line 52, The code currently obtains a writable
pointer via const_cast<char*>(ret.c_str()) which is undefined behavior; change
the pointer acquisition to use a legal mutable buffer (e.g., use ret.data() in
C++17+ or ensure a mutable std::string copy) so that the pointer `p` points to a
modifiable buffer; update the reference where `p` is used accordingly (look for
`char *p = const_cast<char*>(ret.c_str())`, variable `p`, and the string `ret`
in IXBase64.h and replace with a safe approach such as `char *p = ret.data()` or
create a mutable copy before writing).

Comment on lines +54 to +59
for (i = 0; i < in_len - 2; i += 3) {
*p++ = sEncodingTable[(data[i] >> 2) & 0x3F];
*p++ = sEncodingTable[((data[i] & 0x3) << 4) | ((int) (data[i + 1] & 0xF0) >> 4)];
*p++ = sEncodingTable[((data[i + 1] & 0xF) << 2) | ((int) (data[i + 2] & 0xC0) >> 6)];
*p++ = sEncodingTable[data[i + 2] & 0x3F];
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Integer underflow causes out-of-bounds access on small inputs.

The loop condition i < in_len - 2 uses unsigned size_t arithmetic. When in_len is 0, 1, or 2, the expression in_len - 2 underflows to a massive value (~2^64), causing the loop to execute and access data[i] out of bounds.

🐛 Proposed fix to prevent underflow
-    for (i = 0; i < in_len - 2; i += 3) {
+    for (i = 0; in_len >= 3 && i < in_len - 2; i += 3) {

Or restructure to check in_len before entering the loop.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (i = 0; i < in_len - 2; i += 3) {
*p++ = sEncodingTable[(data[i] >> 2) & 0x3F];
*p++ = sEncodingTable[((data[i] & 0x3) << 4) | ((int) (data[i + 1] & 0xF0) >> 4)];
*p++ = sEncodingTable[((data[i + 1] & 0xF) << 2) | ((int) (data[i + 2] & 0xC0) >> 6)];
*p++ = sEncodingTable[data[i + 2] & 0x3F];
}
for (i = 0; in_len >= 3 && i < in_len - 2; i += 3) {
*p++ = sEncodingTable[(data[i] >> 2) & 0x3F];
*p++ = sEncodingTable[((data[i] & 0x3) << 4) | ((int) (data[i + 1] & 0xF0) >> 4)];
*p++ = sEncodingTable[((data[i + 1] & 0xF) << 2) | ((int) (data[i + 2] & 0xC0) >> 6)];
*p++ = sEncodingTable[data[i + 2] & 0x3F];
}
🤖 Prompt for AI Agents
In `@ixwebsocket/IXBase64.h` around lines 54 - 59, The for-loop in the Base64
encoder (the block iterating with i and using data, in_len, and sEncodingTable)
can underflow because it uses the unsigned expression "in_len - 2"; change the
loop guard to a safe check such as testing in_len before the loop or using an
index comparison that cannot underflow (e.g., use "i + 2 < in_len" or
early-return/skip when in_len < 3) so the body that reads data[i], data[i+1],
data[i+2] cannot run on inputs of length 0, 1, or 2.

Comment on lines +96 to +101
size_t in_len = input.size();
if (in_len % 4 != 0) return "Input data size is not a multiple of 4";

size_t out_len = in_len / 4 * 3;
if (input[in_len - 1] == '=') out_len--;
if (input[in_len - 2] == '=') out_len--;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Out-of-bounds read on empty input.

When in_len == 0, the check in_len % 4 != 0 passes (0 % 4 == 0), but lines 100-101 access input[in_len - 1] and input[in_len - 2], which resolve to input[-1] and input[-2] — undefined behavior and memory corruption.

🐛 Proposed fix to handle empty input
     size_t in_len = input.size();
+    if (in_len == 0) {
+      out.clear();
+      return "";
+    }
     if (in_len % 4 != 0) return "Input data size is not a multiple of 4";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
size_t in_len = input.size();
if (in_len % 4 != 0) return "Input data size is not a multiple of 4";
size_t out_len = in_len / 4 * 3;
if (input[in_len - 1] == '=') out_len--;
if (input[in_len - 2] == '=') out_len--;
size_t in_len = input.size();
if (in_len == 0) {
out.clear();
return "";
}
if (in_len % 4 != 0) return "Input data size is not a multiple of 4";
size_t out_len = in_len / 4 * 3;
if (input[in_len - 1] == '=') out_len--;
if (input[in_len - 2] == '=') out_len--;
🤖 Prompt for AI Agents
In `@ixwebsocket/IXBase64.h` around lines 96 - 101, The code reads input[in_len -
1] and input[in_len - 2] without checking for empty input, causing OOB when
in_len == 0; update the start of the block in IXBase64.h (variables input,
in_len, out_len) to handle empty input early—e.g., if in_len == 0 return an
empty output (or otherwise handle as a special case) before computing out_len or
accessing input[in_len - 1/ -2]; alternatively ensure you only access those
indices when in_len >= 2 and adjust out_len logic accordingly.

Comment on lines +106 to +109
uint32_t a = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
uint32_t b = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
uint32_t c = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
uint32_t d = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Signed char causes negative array index — out-of-bounds access.

static_cast<int>(input[i++]) preserves the sign of char on platforms where char is signed. If input contains bytes with the high bit set (values 128-255), the cast produces negative integers, causing kDecodingTable[negative_index] to read before the array — undefined behavior.

🐛 Proposed fix to ensure unsigned indexing
-      uint32_t a = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
-      uint32_t b = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
-      uint32_t c = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
-      uint32_t d = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
+      uint32_t a = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<unsigned char>(input[i++])];
+      uint32_t b = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<unsigned char>(input[i++])];
+      uint32_t c = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<unsigned char>(input[i++])];
+      uint32_t d = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<unsigned char>(input[i++])];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uint32_t a = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
uint32_t b = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
uint32_t c = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
uint32_t d = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<int>(input[i++])];
uint32_t a = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<unsigned char>(input[i++])];
uint32_t b = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<unsigned char>(input[i++])];
uint32_t c = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<unsigned char>(input[i++])];
uint32_t d = input[i] == '=' ? 0 & i++ : kDecodingTable[static_cast<unsigned char>(input[i++])];
🤖 Prompt for AI Agents
In `@ixwebsocket/IXBase64.h` around lines 106 - 109, The code in IXBase64.h reads
kDecodingTable using static_cast<int>(input[i++]) which on platforms with signed
char can produce negative indices and OOB access; update the indexing to first
convert the input byte to an unsigned type before casting to int (e.g. use
static_cast<int>(static_cast<unsigned char>(input[i++])) or cast to uint8_t then
to int) for the four reads that assign a, b, c, d so bytes with high bit set
index kDecodingTable safely while preserving the existing '=' handling logic.

Comment on lines +123 to +134
std::string uri(request->uri);
if (uri.empty() || uri == "/")
{
uri = "/index.html";
}

WebSocketHttpHeaders headers;
headers["Server"] = userAgent();
headers["Content-Type"] = response_head_file(uri);

std::string path("." + uri);
auto res = readAsString(path);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Path traversal vulnerability allows accessing files outside web root.

The URI is directly concatenated to form a file path without sanitization. An attacker can use /../ sequences to escape the current directory and read arbitrary files:

  • Request: GET /../../../etc/passwd HTTP/1.1
  • Constructed path: ./../../../etc/passwd

This is a critical security issue that enables arbitrary file reads from the server filesystem.

🛡️ Proposed fix to sanitize path
+            // Sanitize URI to prevent path traversal
+            std::string sanitizedUri = uri;
+            
+            // Reject URIs with path traversal sequences
+            if (sanitizedUri.find("..") != std::string::npos ||
+                sanitizedUri.find("//") != std::string::npos)
+            {
+                return std::make_shared<HttpResponse>(
+                    403, "Forbidden", HttpErrorCode::Ok, WebSocketHttpHeaders(), std::string());
+            }
+
-                std::string path("." + uri);
+                std::string path("." + sanitizedUri);

Consider also resolving the path canonically and verifying it remains within the intended root directory.

🤖 Prompt for AI Agents
In `@ixwebsocket/IXHttpServer.cpp` around lines 123 - 134, The code builds a file
path by concatenating "." + uri which allows path traversal; sanitize and
canonicalize the URI before calling readAsString. Specifically: strip any
query/fragment from request->uri, percent-decode it, reject or normalize
segments like ".." and empty segments (or collapse them), then join it against a
fixed web root (e.g., webRoot + sanitizedUri) using std::filesystem::path and
obtain a canonical/resolved path (std::filesystem::canonical or equivalent) and
verify the resulting path has the web root as its prefix; only then pass that
canonical path to readAsString(uriPath). Update the logic around variables uri,
path, and the call to readAsString to use the validated canonical path and keep
response_head_file(userAgent()/headers) usage unchanged.

Comment on lines +39 to +49
Uuid::Uuid()
{
static std::random_device rd;
static std::uniform_int_distribution<uint64_t> dist(0, (uint64_t)(~0));

_ab = dist(rd);
_cd = dist(rd);

_ab = (_ab & 0xFFFFFFFFFFFF0FFFULL) | 0x0000000000004000ULL;
_cd = (_cd & 0x3FFFFFFFFFFFFFFFULL) | 0x8000000000000000ULL;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Thread safety: Data race on static random_device and distribution.

The static local variables rd and dist are shared across all threads calling uuid4(). Neither std::random_device nor std::uniform_int_distribution is guaranteed to be thread-safe by the C++ standard, creating a data race when multiple threads generate UUIDs concurrently.

Given the PR's focus on thread safety and race conditions, consider one of these fixes:

  1. Add a mutex to protect access to the static RNG state.
  2. Use thread-local storage (thread_local) to give each thread its own RNG instance.
🔒 Proposed fix using thread_local
     Uuid::Uuid()
     {
-        static std::random_device rd;
-        static std::uniform_int_distribution<uint64_t> dist(0, (uint64_t)(~0));
+        thread_local std::random_device rd;
+        thread_local std::uniform_int_distribution<uint64_t> dist(0, (uint64_t)(~0));
 
         _ab = dist(rd);
         _cd = dist(rd);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Uuid::Uuid()
{
static std::random_device rd;
static std::uniform_int_distribution<uint64_t> dist(0, (uint64_t)(~0));
_ab = dist(rd);
_cd = dist(rd);
_ab = (_ab & 0xFFFFFFFFFFFF0FFFULL) | 0x0000000000004000ULL;
_cd = (_cd & 0x3FFFFFFFFFFFFFFFULL) | 0x8000000000000000ULL;
}
Uuid::Uuid()
{
thread_local std::random_device rd;
thread_local std::uniform_int_distribution<uint64_t> dist(0, (uint64_t)(~0));
_ab = dist(rd);
_cd = dist(rd);
_ab = (_ab & 0xFFFFFFFFFFFF0FFFULL) | 0x0000000000004000ULL;
_cd = (_cd & 0x3FFFFFFFFFFFFFFFULL) | 0x8000000000000000ULL;
}
🤖 Prompt for AI Agents
In `@ixwebsocket/IXUuid.cpp` around lines 39 - 49, The constructor Uuid::Uuid()
currently uses static local rd and dist which causes a data race; make them
thread-local instead (declare rd and dist as thread_local) so each thread has
its own std::random_device and std::uniform_int_distribution instance, then keep
the same calls to dist(rd) and the existing bit-fiddling that sets _ab and _cd;
ensure you only change the storage specifier for rd and dist (or alternatively
protect accesses with a mutex) so uuid generation is safe under concurrent calls
to Uuid::Uuid().

Comment on lines +18 to +56
struct WebSocketMessage
{
WebSocketMessageType type;
const std::string& str;
size_t wireSize;
WebSocketErrorInfo errorInfo;
WebSocketOpenInfo openInfo;
WebSocketCloseInfo closeInfo;
bool binary;

WebSocketMessage(WebSocketMessageType t,
const std::string& s,
size_t w,
WebSocketErrorInfo e,
WebSocketOpenInfo o,
WebSocketCloseInfo c,
bool b = false)
: type(t)
, str(s)
, wireSize(w)
, errorInfo(e)
, openInfo(o)
, closeInfo(c)
, binary(b)
{
;
}

/**
* @brief Deleted overload to prevent binding `str` to a temporary, which would cause
* undefined behavior since class members don't extend lifetime beyond the constructor call.
*/
WebSocketMessage(WebSocketMessageType t,
std::string&& s,
size_t w,
WebSocketErrorInfo e,
WebSocketOpenInfo o,
WebSocketCloseInfo c,
bool b = false) = delete;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find all WebSocketMessage construction sites
rg -n "WebSocketMessage\s*\(" -A 3 -B 1

Repository: RahulRaj272727/IXWebSocket

Length of output: 796


🏁 Script executed:

# Also search for places where WebSocketMessage is instantiated or created
rg -n "WebSocketMessage\(" -A 2

Repository: RahulRaj272727/IXWebSocket

Length of output: 545


🏁 Script executed:

# Look at the header file to understand the full context
head -100 ixwebsocket/IXWebSocketMessage.h

Repository: RahulRaj272727/IXWebSocket

Length of output: 1812


🏁 Script executed:

# Search for WebSocketMessage instantiation in .cpp files
rg -t cpp "WebSocketMessage\s*\{" -A 5 -B 2

Repository: RahulRaj272727/IXWebSocket

Length of output: 52


🏁 Script executed:

# Also search for make_unique or other construction patterns
rg -t cpp "WebSocketMessage" --max-count 20 -B 1 -A 2 | head -80

Repository: RahulRaj272727/IXWebSocket

Length of output: 4697


🏁 Script executed:

# Find all .cpp files in the repository
find . -name "*.cpp" -type f | head -20

Repository: RahulRaj272727/IXWebSocket

Length of output: 672


🏁 Script executed:

# Find where emptyMsg is defined and used
rg -n "emptyMsg" ixwebsocket/IXWebSocket.cpp -B 5 -A 2

Repository: RahulRaj272727/IXWebSocket

Length of output: 1736


🏁 Script executed:

# Look at the specific construction context in IXWebSocket.cpp
rg -n "make_unique<WebSocketMessage>" ixwebsocket/IXWebSocket.cpp -B 8 -A 10 | head -80

Repository: RahulRaj272727/IXWebSocket

Length of output: 3336


🏁 Script executed:

# Find where msg is defined and used in WebSocketMessage construction
rg -n "ix::make_unique<WebSocketMessage>.*msg" ixwebsocket/IXWebSocket.cpp -B 15 -A 3 | head -100

Repository: RahulRaj272727/IXWebSocket

Length of output: 52


🏁 Script executed:

# Also look for the broader context of msg variable definition
rg -n "std::string.*msg" ixwebsocket/IXWebSocket.cpp -B 2 -A 2 | head -80

Repository: RahulRaj272727/IXWebSocket

Length of output: 278


🏁 Script executed:

# Get the full context of where msg is used in WebSocketMessage construction
rg -n "dispatch" ixwebsocket/IXWebSocket.cpp -B 3 -A 30 | head -120

Repository: RahulRaj272727/IXWebSocket

Length of output: 1626


🏁 Script executed:

# Search for the actual WebSocketMessage construction with msg parameter
rg -n "WebSocketMessage.*msg" ixwebsocket/IXWebSocket.cpp -B 10 -A 5

Repository: RahulRaj272727/IXWebSocket

Length of output: 52


🏁 Script executed:

# Get more of the dispatch lambda to see where WebSocketMessage is created with msg
rg -n "dispatch" ixwebsocket/IXWebSocket.cpp -A 50 | grep -A 15 "make_unique<WebSocketMessage>"

Repository: RahulRaj272727/IXWebSocket

Length of output: 828


🏁 Script executed:

# Look for all WebSocketMessage constructions in IXWebSocket.cpp
rg -n "make_unique<WebSocketMessage>" ixwebsocket/IXWebSocket.cpp -A 8

Repository: RahulRaj272727/IXWebSocket

Length of output: 2754


🏁 Script executed:

# Find the dispatch function implementation
rg -n "void.*dispatch\s*\(" ixwebsocket/ -A 20 | head -80

Repository: RahulRaj272727/IXWebSocket

Length of output: 3430


🏁 Script executed:

# Find the WebSocketTransport class to see dispatch signature
rg -n "dispatch\s*\(" ixwebsocket/*.h -A 3

Repository: RahulRaj272727/IXWebSocket

Length of output: 393


🏁 Script executed:

# Get more of the dispatch implementation to see what msg refers to
sed -n '461,700p' ixwebsocket/IXWebSocketTransport.cpp | head -150

Repository: RahulRaj272727/IXWebSocket

Length of output: 6096


🏁 Script executed:

# Search for where onMessageCallback is invoked in dispatch
rg -n "onMessageCallback" ixwebsocket/IXWebSocketTransport.cpp -B 2 -A 2

Repository: RahulRaj272727/IXWebSocket

Length of output: 2005


🏁 Script executed:

# Find the _onMessageCallback definition and usage
rg -n "_onMessageCallback" ixwebsocket/IXWebSocket.h -B 2 -A 5

Repository: RahulRaj272727/IXWebSocket

Length of output: 390


🏁 Script executed:

# Look for setOnMessageCallback to understand what user callbacks might do
rg -n "setOnMessageCallback\|onMessageCallback" ixwebsocket/IXWebSocket.h -B 1 -A 3

Repository: RahulRaj272727/IXWebSocket

Length of output: 52


🏁 Script executed:

# Find the OnMessageCallback type definition
rg -n "OnMessageCallback" ixwebsocket/ --type-list

Repository: RahulRaj272727/IXWebSocket

Length of output: 459


🏁 Script executed:

# Find where OnMessageCallback is defined
rg -n "using OnMessageCallback\|typedef.*OnMessageCallback" ixwebsocket/

Repository: RahulRaj272727/IXWebSocket

Length of output: 52


🏁 Script executed:

# Check IXWebSocket.h for callback registration
rg -n "setOnMessage" ixwebsocket/IXWebSocket.h -A 5

Repository: RahulRaj272727/IXWebSocket

Length of output: 397


🏁 Script executed:

# Find OnMessageCallback typedef
rg -n "OnMessageCallback" ixwebsocket/*.h | grep -E "using|typedef"

Repository: RahulRaj272727/IXWebSocket

Length of output: 249


🏁 Script executed:

# Also check IXWebSocket.h directly for callback types
head -80 ixwebsocket/IXWebSocket.h | tail -40

Repository: RahulRaj272727/IXWebSocket

Length of output: 1649


🏁 Script executed:

# Look at the OnMessageCallback signature in IXWebSocketTransport.h
sed -n '70,75p' ixwebsocket/IXWebSocketTransport.h

Repository: RahulRaj272727/IXWebSocket

Length of output: 342


🏁 Script executed:

# Search for examples of how the callback is used in tests or examples
rg -n "setOnMessageCallback" examples/ -A 5 | head -40

Repository: RahulRaj272727/IXWebSocket

Length of output: 52


Dangling reference in WebSocketMessage::str due to unsafe reference storage.

The const std::string& str member stores a reference to a temporary/local string created in WebSocketTransport::dispatch(). The deleted rvalue overload cannot prevent this—const std::string& still binds to temporaries. When WebSocketMessage objects (via unique_ptr) are passed to user callbacks, the reference may dangle if the callback defers processing. Change to owning storage: std::string str and use std::move() in the constructor initializer list.

🤖 Prompt for AI Agents
In `@ixwebsocket/IXWebSocketMessage.h` around lines 18 - 56, The WebSocketMessage
struct currently stores a const std::string& (member str) which can dangle;
change the member to own the string (std::string str) and update the
constructor(s) to accept an owning parameter (either std::string by value or
std::string&&) and initialize the member with std::move(s) in the initializer
list; remove the deleted rvalue overload since temporaries will now be safely
moved into the owned member. Ensure you update all references to the constructor
signature used by WebSocketTransport::dispatch() so it passes/moves the string
appropriately.

Comment on lines +93 to +96
bool WebSocketPerMessageDeflate::decompress(const std::string& in, std::string& out)
{
return _decompressor->decompress(in, out);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg "class WebSocketPerMessageDeflateDecompressor" -A 50 --type cpp --type h

Repository: RahulRaj272727/IXWebSocket

Length of output: 3090


🏁 Script executed:

rg "decompress" -B 5 -A 30 "WebSocketPerMessageDeflateDecompressor" --type cpp --type h

Repository: RahulRaj272727/IXWebSocket

Length of output: 148


🏁 Script executed:

grep -n "bool WebSocketPerMessageDeflateDecompressor::decompress" -A 50 ixwebsocket/IXWebSocketPerMessageDeflateCodec.cpp

Repository: RahulRaj272727/IXWebSocket

Length of output: 1647


🏁 Script executed:

grep -n "WebSocketPerMessageDeflateOptions" -B 5 -A 20 ixwebsocket/IXWebSocketPerMessageDeflate.h

Repository: RahulRaj272727/IXWebSocket

Length of output: 1063


🏁 Script executed:

grep -r "class WebSocketPerMessageDeflateOptions" -A 30 --type h --type cpp

Repository: RahulRaj272727/IXWebSocket

Length of output: 187


🏁 Script executed:

grep -r "maxMessageSize\|max_decompressed\|decompress_size" --type h --type cpp

Repository: RahulRaj272727/IXWebSocket

Length of output: 187


🏁 Script executed:

rg "class WebSocketPerMessageDeflateOptions" -A 30 --type h

Repository: RahulRaj272727/IXWebSocket

Length of output: 4582


🏁 Script executed:

rg "maxDecompressedSize\|maxMessageSize\|max_decompressed" --type h --type cpp

Repository: RahulRaj272727/IXWebSocket

Length of output: 52


🏁 Script executed:

rg "\.decompress\(" -B 5 -A 10 --type cpp

Repository: RahulRaj272727/IXWebSocket

Length of output: 52


🏁 Script executed:

rg "decompress\(" -B 3 -A 3 --type cpp | head -100

Repository: RahulRaj272727/IXWebSocket

Length of output: 3064


🏁 Script executed:

grep -n "decompress(message, _decompressedMessage)" -B 10 -A 15 ixwebsocket/IXWebSocketTransport.cpp

Repository: RahulRaj272727/IXWebSocket

Length of output: 1285


🏁 Script executed:

rg "maxMessageSize\|max_size\|message.*size.*limit" --type cpp --type h

Repository: RahulRaj272727/IXWebSocket

Length of output: 52


🏁 Script executed:

rg "_decompressedMessage" --type cpp | head -20

Repository: RahulRaj272727/IXWebSocket

Length of output: 530


🏁 Script executed:

rg "size()" --type cpp --type h | grep -i "decompress\|message" | head -20

Repository: RahulRaj272727/IXWebSocket

Length of output: 1967


Decompression lacks size limits and is vulnerable to compression bomb attacks.

The decompress() method in WebSocketPerMessageDeflateDecompressor repeatedly calls inflate() and appends to the output string with no maximum size enforcement. The WebSocketPerMessageDeflateOptions provides no configurable decompression size limit, and at the call site in IXWebSocketTransport::emitMessage(), the decompressed output is only validated for UTF-8 correctness—not size. This allows a small compressed payload to expand into arbitrarily large memory allocations, enabling denial-of-service attacks through compression bombs.

Add a configurable maximum decompressed message size limit to WebSocketPerMessageDeflateOptions and enforce it within the decompression loop to reject payloads that exceed the threshold.

🤖 Prompt for AI Agents
In `@ixwebsocket/IXWebSocketPerMessageDeflate.cpp` around lines 93 - 96, Add a
configurable max-decompressed-size to WebSocketPerMessageDeflateOptions and
enforce it during decompression: extend WebSocketPerMessageDeflateOptions with a
uint64_t maxDecompressedSize (default reasonable value or 0==unlimited), pass
that option into WebSocketPerMessageDeflateDecompressor, and inside
WebSocketPerMessageDeflateDecompressor::decompress (the loop that calls
inflate() and appends to the output) check the cumulative output size after each
inflate chunk and abort/return false if it exceeds maxDecompressedSize; also
ensure WebSocketPerMessageDeflate::decompress forwards failures and that callers
(e.g., IXWebSocketTransport::emitMessage) treat a false return as a
decompression error.

Comment on lines +824 to +830
unsigned WebSocketTransport::getRandomUnsigned()
{
auto now = std::chrono::system_clock::now();
auto seconds =
std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
return static_cast<unsigned>(seconds);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Weak RNG for WebSocket masking keys.

getRandomUnsigned() returns seconds since epoch, which is highly predictable. RFC 6455 Section 5.3 requires that "the masking key needs to be unpredictable" to prevent attackers from predicting the masked payload. Using system time violates this requirement and could enable cache-poisoning or other attacks through intermediary proxies.

Replace with a cryptographically secure random number generator:

🔒 Proposed fix using platform-appropriate CSPRNG
-    unsigned WebSocketTransport::getRandomUnsigned()
-    {
-        auto now = std::chrono::system_clock::now();
-        auto seconds =
-            std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
-        return static_cast<unsigned>(seconds);
-    }
+    unsigned WebSocketTransport::getRandomUnsigned()
+    {
+        std::random_device rd;
+        return rd();
+    }

Note: Include <random> at the top of the file. If std::random_device entropy quality is a concern on certain platforms, consider using OpenSSL's RAND_bytes() or platform-specific APIs like getrandom() / BCryptGenRandom().

🤖 Prompt for AI Agents
In `@ixwebsocket/IXWebSocketTransport.cpp` around lines 824 - 830, The
getRandomUnsigned() implementation uses predictable epoch seconds; replace it
with a cryptographically secure RNG in WebSocketTransport::getRandomUnsigned
(e.g., use std::random_device + std::uniform_int_distribution<unsigned> or call
a platform CSPRNG like getrandom()/BCryptGenRandom()/OpenSSL RAND_bytes() if
stronger entropy is needed), add the appropriate include (<random> or platform
headers), and ensure the returned unsigned is derived from securely generated
bytes rather than time.

uint64_t message_size,
uint8_t masking_key[4]);

unsigned getRandomUnsigned();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the implementation of getRandomUnsigned() for RNG quality
ast-grep --pattern 'unsigned $CLASS::getRandomUnsigned() {
  $$$
}'

# Also search for random-related code in the transport implementation
rg -n "random|mt19937|random_device" --type=cpp -g "IXWebSocketTransport.cpp"

Repository: RahulRaj272727/IXWebSocket

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Find files containing getRandomUnsigned
rg -l "getRandomUnsigned" --type=cpp

# Search for the function definition
rg -n "getRandomUnsigned" --type=cpp -A 10

# Look for random/RNG usage in the codebase
rg -n "random|rand|mt19937|random_device" --type=cpp | head -30

Repository: RahulRaj272727/IXWebSocket

Length of output: 4141


🏁 Script executed:

#!/bin/bash
# Get more context on how getRandomUnsigned is used
rg -n "getRandomUnsigned" --type=cpp -B 5 -A 5

# Check RFC 6455 WebSocket masking requirements in any comments
rg -n "RFC|6455|masking|mask" --type=cpp | head -20

Repository: RahulRaj272727/IXWebSocket

Length of output: 4346


Use cryptographically secure random number generation for WebSocket frame masking keys.

The getRandomUnsigned() implementation (IXWebSocketTransport.cpp:824-830) uses std::chrono::system_clock::now() truncated to seconds as the masking key source. This is predictable and violates RFC 6455, which requires strong randomness for client masking keys. Replace with a proper CSPRNG like std::random_device combined with std::mt19937, similar to the pattern already used elsewhere in the codebase (e.g., IXHttpClient.cpp:778-779).

🤖 Prompt for AI Agents
In `@ixwebsocket/IXWebSocketTransport.h` at line 283, The getRandomUnsigned()
implementation currently uses predictable time-based bits for masking keys;
replace its logic with a cryptographically secure PRNG: initialize a static
std::mt19937 (or std::mt19937_64) seeded from std::random_device and use a
std::uniform_int_distribution<uint32_t> to produce the unsigned masking key in
getRandomUnsigned(); ensure the engine is static/thread-safe (or protected) and
that getRandomUnsigned() returns the uniformly distributed uint32_t value for
WebSocket frame masking keys instead of time-derived values.

@RahulRaj272727
Copy link
Owner Author

@coderabbitai help

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
    • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
    • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant