Commit message: Don't inflate more than ~16mb, drop connection on inflate error
CVE: CVE-2016-10544
CWE: CWE-20
Vulnerability description: uws is a WebSocket server library. By sending a 256mb websocket message to a uws server instance with permessage-deflate enabled, there is a possibility used compression will shrink said 256mb down to less than 16mb of websocket payload which passes the length check of 16mb payload. This data will then inflate up to 256mb and crash the node process by exceeding V8's maximum string size. This affects uws >=0.10.0 <=0.10.8.

Project: uWebSockets
File: WebSocketImpl.cpp

bool WebSocketProtocol<isServer>::refusePayloadLength(void *user, int length) {
    return length > 16777216;
}

Project: uWebSockets
File: Hub.cpp

void Hub::onServerAccept(uS::Socket s) {
    uS::SocketData *socketData = s.getSocketData();
    s.startTimeout<HTTPSocket<SERVER>::onEnd>();
    s.enterState<HTTPSocket<SERVER>>(new HTTPSocket<SERVER>::Data(socketData));
    delete socketData;
}

Project: uWebSockets
File: Hub.cpp

void Hub::onClientConnection(uS::Socket s, bool error) {
    HTTPSocket<CLIENT>::Data *httpSocketData = (HTTPSocket<CLIENT>::Data *) s.getSocketData();

    if (error) {
        ((Group<CLIENT> *) httpSocketData->nodeData)->errorHandler(httpSocketData->httpUser);
        delete httpSocketData;
    } else {
        s.enterState<HTTPSocket<CLIENT>>(s.getSocketData());
        HTTPSocket<CLIENT>(s).upgrade();
    }
}

Project: uWebSockets
File: WebSocketImpl.cpp

void WebSocketProtocol<isServer>::forceClose(void *user) {
    WebSocket<isServer>((uv_poll_t *) user).terminate();
}

Project: uWebSockets
File: Hub.cpp

bool Hub::upgrade(uv_os_sock_t fd, const char *secKey, SSL *ssl, const char *extensions, size_t extensionsLength, Group<SERVER> *serverGroup) {
    if (!serverGroup) {
        serverGroup = &getDefaultGroup<SERVER>();
    }

    uS::Socket s = uS::Socket::init((uS::NodeData *) serverGroup, fd, ssl);
    uS::SocketData *socketData = s.getSocketData();
    HTTPSocket<SERVER>::Data *temporaryHttpData = new HTTPSocket<SERVER>::Data(socketData);
    delete socketData;
    s.enterState<HTTPSocket<SERVER>>(temporaryHttpData);

    // todo: move this into upgrade call
    bool perMessageDeflate = false;
    std::string extensionsResponse;
    if (extensionsLength) {
        ExtensionsNegotiator<uWS::SERVER> extensionsNegotiator(serverGroup->extensionOptions);
        extensionsNegotiator.readOffer(std::string(extensions, extensionsLength));
        extensionsResponse = extensionsNegotiator.generateOffer();
        if (extensionsNegotiator.getNegotiatedOptions() & PERMESSAGE_DEFLATE) {
            perMessageDeflate = true;
        }
    }

    if (HTTPSocket<SERVER>(s).upgrade(secKey, extensionsResponse.data(), extensionsResponse.length())) {
        s.enterState<WebSocket<SERVER>>(new WebSocket<SERVER>::Data(perMessageDeflate, s.getSocketData()));
        serverGroup->addWebSocket(s);
        serverGroup->connectionHandler(WebSocket<SERVER>(s), {nullptr, nullptr, 0, 0});
        delete temporaryHttpData;
        return true;
    }
    return false;
}

Project: uWebSockets
File: Hub.cpp

void Hub::connect(std::string uri, void *user, int timeoutMs, Group<CLIENT> *eh) {
    if (!eh) {
        eh = (Group<CLIENT> *) this;
    }

    int offset = 0;
    std::string protocol = uri.substr(offset, uri.find("://")), hostname, portStr, path;
    if ((offset += protocol.length() + 3) < uri.length()) {
        hostname = uri.substr(offset, uri.find_first_of(":/", offset) - offset);

        offset += hostname.length();
        if (uri[offset] == ':') {
            offset++;
            portStr = uri.substr(offset, uri.find("/", offset) - offset);
        }

        offset += portStr.length();
        if (uri[offset] == '/') {
            path = uri.substr(++offset);
        }
    }

    if (hostname.length()) {
        int port = 80;
        bool secure = false;
        if (protocol == "wss") {
            port = 443;
            secure = true;
        } else if (protocol != "ws") {
            eh->errorHandler(user);
        }

        if (portStr.length()) {
            port = stoi(portStr);
        }

        uS::SocketData socketData((uS::NodeData *) eh);
        HTTPSocket<CLIENT>::Data *httpSocketData = new HTTPSocket<CLIENT>::Data(&socketData);

        httpSocketData->host = hostname;
        httpSocketData->path = path;
        httpSocketData->httpUser = user;

        uS::Socket s = uS::Node::connect<onClientConnection>(hostname.c_str(), port, secure, httpSocketData);
        if (s) {
            s.startTimeout<HTTPSocket<CLIENT>::onEnd>(timeoutMs);
        }
    } else {
        eh->errorHandler(user);
    }
}

Project: uWebSockets
File: WebSocketImpl.cpp

bool WebSocketProtocol<isServer>::handleFragment(char *data, size_t length, unsigned int remainingBytes, int opCode, bool fin, void *user) {
    uS::Socket s((uv_poll_t *) user);
    typename WebSocket<isServer>::Data *webSocketData = (typename WebSocket<isServer>::Data *) s.getSocketData();

    if (opCode < 3) {
        if (!remainingBytes && fin && !webSocketData->fragmentBuffer.length()) {
            if (webSocketData->compressionStatus == WebSocket<isServer>::Data::CompressionStatus::COMPRESSED_FRAME) {
                webSocketData->compressionStatus = WebSocket<isServer>::Data::CompressionStatus::ENABLED;
                Hub *hub = ((Group<isServer> *) s.getSocketData()->nodeData)->hub;
                data = hub->inflate(data, length);
                if (!data) {
                    forceClose(user);
                    return true;
                }
            }

            if (opCode == 1 && !isValidUtf8((unsigned char *) data, length)) {
                forceClose(user);
                return true;
            }

            ((Group<isServer> *) s.getSocketData()->nodeData)->messageHandler(WebSocket<isServer>(s), data, length, (OpCode) opCode);
            if (s.isClosed() || s.isShuttingDown()) {
                return true;
            }
        } else {
            webSocketData->fragmentBuffer.append(data, length);
            if (!remainingBytes && fin) {
                length = webSocketData->fragmentBuffer.length();
                if (webSocketData->compressionStatus == WebSocket<isServer>::Data::CompressionStatus::COMPRESSED_FRAME) {
                    webSocketData->compressionStatus = WebSocket<isServer>::Data::CompressionStatus::ENABLED;
                    Hub *hub = ((Group<isServer> *) s.getSocketData()->nodeData)->hub;
                    webSocketData->fragmentBuffer.append("....");
                    data = hub->inflate((char *) webSocketData->fragmentBuffer.data(), length);
                    if (!data) {
                        forceClose(user);
                        return true;
                    }
                } else {
                    data = (char *) webSocketData->fragmentBuffer.data();
                }

                if (opCode == 1 && !isValidUtf8((unsigned char *) data, length)) {
                    forceClose(user);
                    return true;
                }

                ((Group<isServer> *) s.getSocketData()->nodeData)->messageHandler(WebSocket<isServer>(s), data, length, (OpCode) opCode);
                if (s.isClosed() || s.isShuttingDown()) {
                    return true;
                }
                webSocketData->fragmentBuffer.clear();
            }
        }
    } else {
        // todo: we don't need to buffer up in most cases!
        webSocketData->controlBuffer.append(data, length);
        if (!remainingBytes && fin) {
            if (opCode == CLOSE) {
                CloseFrame closeFrame = parseClosePayload((char *) webSocketData->controlBuffer.data(), webSocketData->controlBuffer.length());
                WebSocket<isServer>(s).close(closeFrame.code, closeFrame.message, closeFrame.length);
                return true;
            } else {
                if (opCode == PING) {
                    WebSocket<isServer>(s).send(webSocketData->controlBuffer.data(), webSocketData->controlBuffer.length(), (OpCode) OpCode::PONG);
                    ((Group<isServer> *) s.getSocketData()->nodeData)->pingHandler(WebSocket<isServer>(s), (char *) webSocketData->controlBuffer.data(), webSocketData->controlBuffer.length());
                    if (s.isClosed() || s.isShuttingDown()) {
                        return true;
                    }
                } else if (opCode == PONG) {
                    ((Group<isServer> *) s.getSocketData()->nodeData)->pongHandler(WebSocket<isServer>(s), (char *) webSocketData->controlBuffer.data(), webSocketData->controlBuffer.length());
                    if (s.isClosed() || s.isShuttingDown()) {
                        return true;
                    }
                }
            }
            webSocketData->controlBuffer.clear();
        }
    }

    return false;
}

Project: uWebSockets
File: WebSocketImpl.cpp

bool WebSocketProtocol<isServer>::setCompressed(void *user) {
    uS::Socket s((uv_poll_t *) user);
    typename WebSocket<isServer>::Data *webSocketData = (typename WebSocket<isServer>::Data *) s.getSocketData();

    if (webSocketData->compressionStatus == WebSocket<isServer>::Data::CompressionStatus::ENABLED) {
        webSocketData->compressionStatus = WebSocket<isServer>::Data::CompressionStatus::COMPRESSED_FRAME;
        return true;
    } else {
        return false;
    }
}

Project: uWebSockets
File: Hub.cpp

char *Hub::inflate(char *data, size_t &length) {
    dynamicInflationBuffer.clear();

    inflationStream.next_in = (Bytef *) data;
    inflationStream.avail_in = length;

    int err;
    do {
        inflationStream.next_out = (Bytef *) inflationBuffer;
        inflationStream.avail_out = LARGE_BUFFER_SIZE;
        err = ::inflate(&inflationStream, Z_FINISH);
        if (!inflationStream.avail_in) {
            break;
        }

        dynamicInflationBuffer.append(inflationBuffer, LARGE_BUFFER_SIZE - inflationStream.avail_out);
    } while (err == Z_BUF_ERROR && dynamicInflationBuffer.length() <= INFLATE_LESS_THAN_ROUGHLY);

    inflateReset(&inflationStream);

    if ((err != Z_BUF_ERROR && err != Z_OK) || dynamicInflationBuffer.length() > INFLATE_LESS_THAN_ROUGHLY) {
        length = 0;
        return nullptr;
    }

    if (dynamicInflationBuffer.length()) {
        dynamicInflationBuffer.append(inflationBuffer, LARGE_BUFFER_SIZE - inflationStream.avail_out);

        length = dynamicInflationBuffer.length();
        return (char *) dynamicInflationBuffer.data();
    }

    length = LARGE_BUFFER_SIZE - inflationStream.avail_out;
    return inflationBuffer;
}

Project: uWebSockets
File: Hub.cpp

bool Hub::listen(int port, uS::TLS::Context sslContext, int options, Group<SERVER> *eh) {
    if (!eh) {
        eh = (Group<SERVER> *) this;
    }

    if (uS::Node::listen<onServerAccept>(port, sslContext, options, (uS::NodeData *) eh, nullptr)) {
        eh->errorHandler(port);
        return false;
    }
    return true;
}