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; return m_serverAddress;
} }
size_t Client::getMaximumClipboardSizeBytes() const
{
return m_maximumClipboardSize * 1024;
}
void *Client::getEventTarget() const void *Client::getEventTarget() const
{ {
return m_screen->getEventTarget(); return m_screen->getEventTarget();

View file

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

View file

@ -530,24 +530,29 @@ void ServerProxy::leave()
void ServerProxy::setClipboard() void ServerProxy::setClipboard()
{ {
// parse // parse
static std::string dataCached;
ClipboardID id; ClipboardID id;
uint32_t seq; 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) { if (r == TransferState::Started) {
size_t size = ClipboardChunk::getExpectedSize(); size_t size = ClipboardChunk::getExpectedSize(m_clipboardChunkState);
LOG_DEBUG("receiving clipboard %d size=%d", id, size); LOG_DEBUG("receiving clipboard %d size=%zu", id, size);
} else if (r == TransferState::Finished) { } 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 // forward
Clipboard clipboard; Clipboard clipboard;
clipboard.unmarshall(dataCached, 0); clipboard.unmarshall(m_clipboardDataCached, 0);
m_client->setClipboard(id, &clipboard); m_client->setClipboard(id, &clipboard);
m_clipboardDataCached.clear();
m_clipboardDataCached.shrink_to_fit();
LOG_INFO("clipboard was updated"); 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 #pragma once
#include "deskflow/ClipboardChunk.h"
#include "deskflow/ClipboardTypes.h" #include "deskflow/ClipboardTypes.h"
#include "deskflow/KeyTypes.h" #include "deskflow/KeyTypes.h"
#include "deskflow/KeyboardLayoutManager.h" #include "deskflow/KeyboardLayoutManager.h"
@ -124,6 +125,8 @@ private:
MessageParser m_parser = &ServerProxy::parseHandshakeMessage; MessageParser m_parser = &ServerProxy::parseHandshakeMessage;
IEventQueue *m_events = nullptr; IEventQueue *m_events = nullptr;
std::string m_serverLayout = ""; std::string m_serverLayout = "";
std::string m_clipboardDataCached;
ClipboardChunkAssemblyState m_clipboardChunkState;
bool m_isUserNotifiedAboutLayoutSyncError = false; bool m_isUserNotifiedAboutLayoutSyncError = false;
deskflow::KeyboardLayoutManager m_layoutManager; deskflow::KeyboardLayoutManager m_layoutManager;
}; };

View file

@ -12,8 +12,22 @@
#include "deskflow/ProtocolUtil.h" #include "deskflow/ProtocolUtil.h"
#include "io/IStream.h" #include "io/IStream.h"
#include <cstring> #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) ClipboardChunk::ClipboardChunk(size_t size) : Chunk(size)
{ {
@ -62,37 +76,88 @@ ClipboardChunk *ClipboardChunk::end(ClipboardID id, uint32_t sequence)
return end; return end;
} }
TransferState TransferState ClipboardChunk::assemble(
ClipboardChunk::assemble(deskflow::IStream *stream, std::string &dataCached, ClipboardID &id, uint32_t &sequence) deskflow::IStream *stream, std::string &dataCached, ClipboardID &id, uint32_t &sequence,
ClipboardChunkAssemblyState &state, size_t maxDataSize
)
{ {
using enum TransferState; using enum TransferState;
uint8_t mark; uint8_t mark;
std::string data; std::string data;
auto reset = [&]() {
state = {};
clearCachedData(dataCached);
};
if (!ProtocolUtil::readf(stream, kMsgDClipboard + 4, &id, &sequence, &mark, &data)) { 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; return Error;
} }
if (mark == ChunkType::DataStart) { if (mark == ChunkType::DataStart) {
s_expectedSize = QString::fromStdString(data).toULong(); bool ok = false;
LOG_DEBUG("start receiving clipboard data"); const auto expected = QString::fromStdString(data).toULongLong(&ok);
dataCached.clear(); 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; return Started;
} else if (mark == ChunkType::DataChunk) { } 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); dataCached.append(data);
return TransferState::InProgress; return TransferState::InProgress;
} else if (mark == ChunkType::DataEnd) { } else if (mark == ChunkType::DataEnd) {
// validate if (!state.active) {
if (id >= kClipboardEnd) { LOG_ERR("clipboard end chunk before start");
reset();
return Error; 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 Error;
} }
return Finished; return Finished;
} }
LOG_ERR("clipboard transmission failed: unknown error"); LOG_ERR("unknown clipboard chunk mark");
reset();
return Error; return Error;
} }

View file

@ -10,6 +10,7 @@
#include "deskflow/ClipboardTypes.h" #include "deskflow/ClipboardTypes.h"
#include "deskflow/ProtocolTypes.h" #include "deskflow/ProtocolTypes.h"
#include <cstddef>
#include <string> #include <string>
constexpr static auto s_clipboardChunkMetaSize = 7; constexpr static auto s_clipboardChunkMetaSize = 7;
@ -18,6 +19,12 @@ namespace deskflow {
class IStream; class IStream;
} }
struct ClipboardChunkAssemblyState
{
size_t expectedSize = 0;
bool active = false;
};
class ClipboardChunk : public Chunk class ClipboardChunk : public Chunk
{ {
public: public:
@ -27,16 +34,15 @@ public:
static ClipboardChunk *data(ClipboardID id, uint32_t sequence, const std::string &data); static ClipboardChunk *data(ClipboardID id, uint32_t sequence, const std::string &data);
static ClipboardChunk *end(ClipboardID id, uint32_t sequence); static ClipboardChunk *end(ClipboardID id, uint32_t sequence);
static TransferState static TransferState assemble(
assemble(deskflow::IStream *stream, std::string &dataCached, ClipboardID &id, uint32_t &sequence); 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 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() bool ClientProxy1_6::recvClipboard()
{ {
// parse message // parse message
static std::string dataCached;
ClipboardID id; ClipboardID id;
uint32_t seq; uint32_t seq;
if (auto r = ClipboardChunk::assemble(getStream(), dataCached, id, seq); r == TransferState::Started) { auto r = ClipboardChunk::assemble(
size_t size = ClipboardChunk::getExpectedSize(); getStream(), m_clipboardDataCached, id, seq, m_clipboardChunkState, m_server->getMaximumClipboardSizeBytes()
LOG_DEBUG("receiving clipboard %d size=%d", id, size); );
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) { } else if (r == TransferState::Finished) {
LOG( LOG(
(CLOG_DEBUG "received client \"%s\" clipboard %d seqnum=%d, size=%d", getName().c_str(), id, seq, (CLOG_DEBUG "received client \"%s\" clipboard %d seqnum=%d, size=%zu", getName().c_str(), id, seq,
dataCached.size()) m_clipboardDataCached.size())
); );
// save clipboard // 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_clipboard[id].m_sequenceNumber = seq;
m_clipboardDataCached.clear();
m_clipboardDataCached.shrink_to_fit();
// notify // notify
auto *info = new ClipboardInfo; auto *info = new ClipboardInfo;
info->m_id = id; info->m_id = id;
info->m_sequenceNumber = seq; info->m_sequenceNumber = seq;
m_events->addEvent(Event(EventTypes::ClipboardChanged, getEventTarget(), info)); m_events->addEvent(Event(EventTypes::ClipboardChanged, getEventTarget(), info));
} else if (r == TransferState::Error) {
return false;
} }
return true; return true;

View file

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

View file

@ -183,6 +183,11 @@ Server::~Server()
removeClient(m_primaryClient); removeClient(m_primaryClient);
} }
size_t Server::getMaximumClipboardSizeBytes() const
{
return m_maximumClipboardSize * 1024;
}
bool Server::setConfig(const ServerConfig &config) bool Server::setConfig(const ServerConfig &config)
{ {
// refuse configuration if it doesn't include the primary screen // 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 getClients(std::vector<std::string> &list) const;
void sendConnectedClientsIpc() const; void sendConnectedClientsIpc() const;
size_t getMaximumClipboardSizeBytes() const;
//@} //@}

View file

@ -9,6 +9,162 @@
#include "deskflow/ClipboardChunk.h" #include "deskflow/ClipboardChunk.h"
#include "deskflow/ProtocolTypes.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() void ClipboardChunksTests::startFormatData()
{ {
@ -70,4 +226,63 @@ void ClipboardChunksTests::endFormatData()
delete chunk; 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) QTEST_MAIN(ClipboardChunksTests)

View file

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