fix: enforce clipboard receive size limits

This commit is contained in:
BeaCox 2026-07-03 21:12:49 +08:00 committed by Nick Bolton
parent 205a3c803e
commit 8a535fd5dd
12 changed files with 358 additions and 31 deletions

View file

@ -175,6 +175,11 @@ NetworkAddress Client::getServerAddress() const
return m_serverAddress;
}
size_t Client::getMaximumClipboardSizeBytes() const
{
return m_maximumClipboardSize * 1024;
}
void *Client::getEventTarget() const
{
return m_screen->getEventTarget();

View file

@ -126,6 +126,7 @@ public:
{
return m_resolvedAddressesCount;
}
size_t getMaximumClipboardSizeBytes() const;
//@}

View file

@ -530,24 +530,29 @@ void ServerProxy::leave()
void ServerProxy::setClipboard()
{
// parse
static std::string dataCached;
ClipboardID id;
uint32_t seq;
auto r = ClipboardChunk::assemble(m_stream, dataCached, id, seq);
auto r = ClipboardChunk::assemble(
m_stream, m_clipboardDataCached, id, seq, m_clipboardChunkState, m_client->getMaximumClipboardSizeBytes()
);
if (r == TransferState::Started) {
size_t size = ClipboardChunk::getExpectedSize();
LOG_DEBUG("receiving clipboard %d size=%d", id, size);
size_t size = ClipboardChunk::getExpectedSize(m_clipboardChunkState);
LOG_DEBUG("receiving clipboard %d size=%zu", id, size);
} else if (r == TransferState::Finished) {
LOG_DEBUG("received clipboard %d size=%d", id, dataCached.size());
LOG_DEBUG("received clipboard %d size=%zu", id, m_clipboardDataCached.size());
// forward
Clipboard clipboard;
clipboard.unmarshall(dataCached, 0);
clipboard.unmarshall(m_clipboardDataCached, 0);
m_client->setClipboard(id, &clipboard);
m_clipboardDataCached.clear();
m_clipboardDataCached.shrink_to_fit();
LOG_INFO("clipboard was updated");
} else if (r == TransferState::Error) {
m_client->disconnect("invalid clipboard data from server");
}
}

View file

@ -8,6 +8,7 @@
#pragma once
#include "deskflow/ClipboardChunk.h"
#include "deskflow/ClipboardTypes.h"
#include "deskflow/KeyTypes.h"
#include "deskflow/KeyboardLayoutManager.h"
@ -124,6 +125,8 @@ private:
MessageParser m_parser = &ServerProxy::parseHandshakeMessage;
IEventQueue *m_events = nullptr;
std::string m_serverLayout = "";
std::string m_clipboardDataCached;
ClipboardChunkAssemblyState m_clipboardChunkState;
bool m_isUserNotifiedAboutLayoutSyncError = false;
deskflow::KeyboardLayoutManager m_layoutManager;
};

View file

@ -12,8 +12,22 @@
#include "deskflow/ProtocolUtil.h"
#include "io/IStream.h"
#include <cstring>
#include <limits>
size_t ClipboardChunk::s_expectedSize = 0;
namespace {
void clearCachedData(std::string &dataCached)
{
dataCached.clear();
dataCached.shrink_to_fit();
}
bool wouldExceed(size_t currentSize, size_t extraSize, size_t limit)
{
return currentSize > limit || extraSize > limit - currentSize;
}
} // namespace
ClipboardChunk::ClipboardChunk(size_t size) : Chunk(size)
{
@ -62,37 +76,88 @@ ClipboardChunk *ClipboardChunk::end(ClipboardID id, uint32_t sequence)
return end;
}
TransferState
ClipboardChunk::assemble(deskflow::IStream *stream, std::string &dataCached, ClipboardID &id, uint32_t &sequence)
TransferState ClipboardChunk::assemble(
deskflow::IStream *stream, std::string &dataCached, ClipboardID &id, uint32_t &sequence,
ClipboardChunkAssemblyState &state, size_t maxDataSize
)
{
using enum TransferState;
uint8_t mark;
std::string data;
auto reset = [&]() {
state = {};
clearCachedData(dataCached);
};
if (!ProtocolUtil::readf(stream, kMsgDClipboard + 4, &id, &sequence, &mark, &data)) {
reset();
return Error;
}
if (id >= kClipboardEnd) {
LOG_ERR("clipboard chunk invalid id: %d", id);
reset();
return Error;
}
if (mark == ChunkType::DataStart) {
s_expectedSize = QString::fromStdString(data).toULong();
LOG_DEBUG("start receiving clipboard data");
dataCached.clear();
bool ok = false;
const auto expected = QString::fromStdString(data).toULongLong(&ok);
if (!ok || expected > std::numeric_limits<size_t>::max()) {
LOG_ERR("clipboard invalid size header: %s", data.c_str());
reset();
return Error;
}
clearCachedData(dataCached);
state.expectedSize = static_cast<size_t>(expected);
state.active = true;
if (state.expectedSize > maxDataSize) {
LOG_ERR("clipboard size exceeds limit, size: %zu, limit: %zu", state.expectedSize, maxDataSize);
reset();
return Error;
}
LOG_DEBUG("start receiving clipboard data, expected size=%zu", state.expectedSize);
return Started;
} else if (mark == ChunkType::DataChunk) {
if (!state.active) {
LOG_ERR("clipboard data chunk before start");
reset();
return Error;
}
if (wouldExceed(dataCached.size(), data.size(), state.expectedSize)) {
LOG_ERR(
"clipboard size exceeds declared, size: %zu, declared: %zu", dataCached.size() + data.size(),
state.expectedSize
);
reset();
return Error;
}
dataCached.append(data);
return TransferState::InProgress;
} else if (mark == ChunkType::DataEnd) {
// validate
if (id >= kClipboardEnd) {
if (!state.active) {
LOG_ERR("clipboard end chunk before start");
reset();
return Error;
} else if (s_expectedSize != dataCached.size()) {
LOG_ERR("corrupted clipboard data, expected size=%d actual size=%d", s_expectedSize, dataCached.size());
}
state.active = false;
if (state.expectedSize != dataCached.size()) {
LOG_ERR("corrupted clipboard data, expected size=%zu actual size=%zu", state.expectedSize, dataCached.size());
reset();
return Error;
}
return Finished;
}
LOG_ERR("clipboard transmission failed: unknown error");
LOG_ERR("unknown clipboard chunk mark");
reset();
return Error;
}

View file

@ -10,6 +10,7 @@
#include "deskflow/ClipboardTypes.h"
#include "deskflow/ProtocolTypes.h"
#include <cstddef>
#include <string>
constexpr static auto s_clipboardChunkMetaSize = 7;
@ -18,6 +19,12 @@ namespace deskflow {
class IStream;
}
struct ClipboardChunkAssemblyState
{
size_t expectedSize = 0;
bool active = false;
};
class ClipboardChunk : public Chunk
{
public:
@ -27,16 +34,15 @@ public:
static ClipboardChunk *data(ClipboardID id, uint32_t sequence, const std::string &data);
static ClipboardChunk *end(ClipboardID id, uint32_t sequence);
static TransferState
assemble(deskflow::IStream *stream, std::string &dataCached, ClipboardID &id, uint32_t &sequence);
static TransferState assemble(
deskflow::IStream *stream, std::string &dataCached, ClipboardID &id, uint32_t &sequence,
ClipboardChunkAssemblyState &state, size_t maxDataSize
);
static void send(deskflow::IStream *stream, void *data);
static size_t getExpectedSize()
static size_t getExpectedSize(const ClipboardChunkAssemblyState &state)
{
return s_expectedSize;
return state.expectedSize;
}
private:
static size_t s_expectedSize;
};

View file

@ -51,27 +51,34 @@ void ClientProxy1_6::setClipboard(ClipboardID id, const IClipboard *clipboard)
bool ClientProxy1_6::recvClipboard()
{
// parse message
static std::string dataCached;
ClipboardID id;
uint32_t seq;
if (auto r = ClipboardChunk::assemble(getStream(), dataCached, id, seq); r == TransferState::Started) {
size_t size = ClipboardChunk::getExpectedSize();
LOG_DEBUG("receiving clipboard %d size=%d", id, size);
auto r = ClipboardChunk::assemble(
getStream(), m_clipboardDataCached, id, seq, m_clipboardChunkState, m_server->getMaximumClipboardSizeBytes()
);
if (r == TransferState::Started) {
size_t size = ClipboardChunk::getExpectedSize(m_clipboardChunkState);
LOG_DEBUG("receiving clipboard %d size=%zu", id, size);
} else if (r == TransferState::Finished) {
LOG(
(CLOG_DEBUG "received client \"%s\" clipboard %d seqnum=%d, size=%d", getName().c_str(), id, seq,
dataCached.size())
(CLOG_DEBUG "received client \"%s\" clipboard %d seqnum=%d, size=%zu", getName().c_str(), id, seq,
m_clipboardDataCached.size())
);
// save clipboard
m_clipboard[id].m_clipboard.unmarshall(dataCached, 0);
m_clipboard[id].m_clipboard.unmarshall(m_clipboardDataCached, 0);
m_clipboard[id].m_sequenceNumber = seq;
m_clipboardDataCached.clear();
m_clipboardDataCached.shrink_to_fit();
// notify
auto *info = new ClipboardInfo;
info->m_id = id;
info->m_sequenceNumber = seq;
m_events->addEvent(Event(EventTypes::ClipboardChanged, getEventTarget(), info));
} else if (r == TransferState::Error) {
return false;
}
return true;

View file

@ -6,8 +6,11 @@
#pragma once
#include "deskflow/ClipboardChunk.h"
#include "server/ClientProxy1_5.h"
#include <string>
class Server;
class IEventQueue;
@ -23,4 +26,6 @@ public:
private:
IEventQueue *m_events;
std::string m_clipboardDataCached;
ClipboardChunkAssemblyState m_clipboardChunkState;
};

View file

@ -183,6 +183,11 @@ Server::~Server()
removeClient(m_primaryClient);
}
size_t Server::getMaximumClipboardSizeBytes() const
{
return m_maximumClipboardSize * 1024;
}
bool Server::setConfig(const ServerConfig &config)
{
// refuse configuration if it doesn't include the primary screen

View file

@ -198,6 +198,7 @@ public:
*/
void getClients(std::vector<std::string> &list) const;
void sendConnectedClientsIpc() const;
size_t getMaximumClipboardSizeBytes() const;
//@}

View file

@ -9,6 +9,162 @@
#include "deskflow/ClipboardChunk.h"
#include "deskflow/ProtocolTypes.h"
#include "deskflow/ProtocolUtil.h"
#include "io/IStream.h"
#include <algorithm>
#include <cstring>
#include <deque>
namespace {
class MemoryStream : public deskflow::IStream
{
public:
void push(const std::string &bytes)
{
m_queue.push_back(bytes);
}
void close() override
{
m_queue.clear();
m_inputShutdown = true;
}
uint32_t read(void *buffer, uint32_t n) override
{
if (m_inputShutdown || m_queue.empty() || n == 0) {
return 0;
}
auto &front = m_queue.front();
const size_t take = std::min(static_cast<size_t>(n), front.size());
if (buffer != nullptr) {
std::memcpy(buffer, front.data(), take);
}
front.erase(0, take);
if (front.empty()) {
m_queue.pop_front();
}
return static_cast<uint32_t>(take);
}
void write(const void *, uint32_t) override
{
}
void flush() override
{
}
void shutdownInput() override
{
close();
}
void shutdownOutput() override
{
}
void *getEventTarget() const override
{
return const_cast<MemoryStream *>(this);
}
bool isReady() const override
{
return !m_inputShutdown && !m_queue.empty();
}
uint32_t getSize() const override
{
size_t total = 0;
for (const auto &chunk : m_queue) {
total += chunk.size();
}
return static_cast<uint32_t>(std::min<size_t>(total, UINT32_MAX));
}
private:
std::deque<std::string> m_queue;
bool m_inputShutdown = false;
};
class BufferWriteStream : public deskflow::IStream
{
public:
const std::string &str() const
{
return m_buffer;
}
void close() override
{
m_outputShutdown = true;
}
uint32_t read(void *, uint32_t) override
{
return 0;
}
void write(const void *buffer, uint32_t n) override
{
if (!m_outputShutdown && n != 0) {
m_buffer.append(static_cast<const char *>(buffer), n);
}
}
void flush() override
{
}
void shutdownInput() override
{
}
void shutdownOutput() override
{
m_outputShutdown = true;
}
void *getEventTarget() const override
{
return const_cast<BufferWriteStream *>(this);
}
bool isReady() const override
{
return false;
}
uint32_t getSize() const override
{
return 0;
}
private:
std::string m_buffer;
bool m_outputShutdown = false;
};
std::string encodeClipboardMsg(ClipboardID id, uint32_t seq, uint8_t mark, const std::string &data)
{
BufferWriteStream stream;
auto payload = data;
ProtocolUtil::writef(&stream, kMsgDClipboard + 4, id, seq, mark, &payload);
return stream.str();
}
} // namespace
void ClipboardChunksTests::initTestCase()
{
m_log.setFilter(LogLevel::Level::Debug);
}
void ClipboardChunksTests::startFormatData()
{
@ -70,4 +226,63 @@ void ClipboardChunksTests::endFormatData()
delete chunk;
}
void ClipboardChunksTests::assembleAllowsDataAtExpectedSizeAndLimit()
{
MemoryStream stream;
stream.push(encodeClipboardMsg(0, 7, ChunkType::DataStart, "4"));
stream.push(encodeClipboardMsg(0, 7, ChunkType::DataChunk, "AB"));
stream.push(encodeClipboardMsg(0, 7, ChunkType::DataChunk, "CD"));
stream.push(encodeClipboardMsg(0, 7, ChunkType::DataEnd, ""));
std::string cached;
ClipboardID id = kClipboardEnd;
uint32_t seq = 0;
ClipboardChunkAssemblyState state;
QCOMPARE(ClipboardChunk::assemble(&stream, cached, id, seq, state, 4), TransferState::Started);
QCOMPARE(ClipboardChunk::assemble(&stream, cached, id, seq, state, 4), TransferState::InProgress);
QCOMPARE(ClipboardChunk::assemble(&stream, cached, id, seq, state, 4), TransferState::InProgress);
QCOMPARE(ClipboardChunk::assemble(&stream, cached, id, seq, state, 4), TransferState::Finished);
QCOMPARE(cached, std::string("ABCD"));
QCOMPARE(id, static_cast<ClipboardID>(0));
QCOMPARE(seq, static_cast<uint32_t>(7));
QCOMPARE(ClipboardChunk::getExpectedSize(state), static_cast<size_t>(4));
QVERIFY(!state.active);
}
void ClipboardChunksTests::assembleRejectsDataBeyondExpectedSize()
{
MemoryStream stream;
stream.push(encodeClipboardMsg(0, 7, ChunkType::DataStart, "1"));
stream.push(encodeClipboardMsg(0, 7, ChunkType::DataChunk, "AA"));
std::string cached;
ClipboardID id = kClipboardEnd;
uint32_t seq = 0;
ClipboardChunkAssemblyState state;
QCOMPARE(ClipboardChunk::assemble(&stream, cached, id, seq, state, 1024), TransferState::Started);
QCOMPARE(ClipboardChunk::assemble(&stream, cached, id, seq, state, 1024), TransferState::Error);
QVERIFY(cached.empty());
QCOMPARE(ClipboardChunk::getExpectedSize(state), static_cast<size_t>(0));
QVERIFY(!state.active);
}
void ClipboardChunksTests::assembleRejectsExpectedSizeBeyondLimit()
{
MemoryStream stream;
stream.push(encodeClipboardMsg(0, 7, ChunkType::DataStart, "8"));
std::string cached;
ClipboardID id = kClipboardEnd;
uint32_t seq = 0;
ClipboardChunkAssemblyState state;
QCOMPARE(ClipboardChunk::assemble(&stream, cached, id, seq, state, 4), TransferState::Error);
QVERIFY(cached.empty());
QCOMPARE(ClipboardChunk::getExpectedSize(state), static_cast<size_t>(0));
QVERIFY(!state.active);
}
QTEST_MAIN(ClipboardChunksTests)

View file

@ -4,6 +4,8 @@
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "base/Log.h"
#include <QTest>
class ClipboardChunksTests : public QObject
@ -11,7 +13,14 @@ class ClipboardChunksTests : public QObject
Q_OBJECT
private Q_SLOTS:
// Test are run in order top to bottom
void initTestCase();
void startFormatData();
void formatDataChunk();
void endFormatData();
void assembleAllowsDataAtExpectedSizeAndLimit();
void assembleRejectsDataBeyondExpectedSize();
void assembleRejectsExpectedSizeBeyondLimit();
private:
Log m_log;
};