Security-хардening: XSS в никах, non-root контейнер, rate-limiting, спавн-фикс
Клиент: ники других игроков рендерились через innerHTML без экранирования — XSS. Добавлен escapeHtml() для nickname и в списке игроков комнаты. Сервер: - Санитайзинг ника и на бэкенде (обрезка длины, вырезание control/HTML символов) — защита не только для этого клиента, но и для любых будущих потребителей (логи, другие рендеры). - onMessage обёрнут в try/catch: payload.value<T>() кидает json::type_error на несовпадении типа поля — одно кривое сообщение могло уронить процесс со всеми активными играми. - Лимит на общее число комнат (200) — иначе клиент, спамящий lobby.create_room, разгонял rooms_ до исчерпания памяти. - Fix: одна из раскладок coop-карты клала укрытие поверх стартовой позиции игрока — танк рождался внутри кирпича и не мог выехать. Тайл под каждым стартовым танком теперь расчищается после построения карты. Docker: gameserver теперь работает от непривилегированного пользователя, а не от root. nginx: rate-limiting на /ws (5 подключений/с на IP, до 20 одновременных) — защита от флуда соединениями/комнатами; CSP и security-заголовки (X-Content-Type-Options, X-Frame-Options, Referrer-Policy).
This commit is contained in:
+53
-17
@@ -92,31 +92,63 @@ void Lobby::onMessage(WS *ws, std::string_view message) {
|
||||
|
||||
json payload = request.value("payload", json::object());
|
||||
|
||||
if (type == "hello") {
|
||||
handleHello(ws, payload);
|
||||
} else if (type == "lobby.list_rooms") {
|
||||
handleListRooms(ws);
|
||||
} else if (type == "lobby.create_room") {
|
||||
handleCreateRoom(ws, payload);
|
||||
} else if (type == "lobby.join_room") {
|
||||
handleJoinRoom(ws, payload);
|
||||
} else if (type == "lobby.leave_room") {
|
||||
handleLeaveRoom(ws);
|
||||
} else if (type == "game.ready") {
|
||||
handleGameReady(ws);
|
||||
} else if (type == "game.input") {
|
||||
handleGameInput(ws, payload);
|
||||
} else {
|
||||
sendError(ws, "unknown_type", "unknown message type: " + type);
|
||||
try {
|
||||
if (type == "hello") {
|
||||
handleHello(ws, payload);
|
||||
} else if (type == "lobby.list_rooms") {
|
||||
handleListRooms(ws);
|
||||
} else if (type == "lobby.create_room") {
|
||||
handleCreateRoom(ws, payload);
|
||||
} else if (type == "lobby.join_room") {
|
||||
handleJoinRoom(ws, payload);
|
||||
} else if (type == "lobby.leave_room") {
|
||||
handleLeaveRoom(ws);
|
||||
} else if (type == "game.ready") {
|
||||
handleGameReady(ws);
|
||||
} else if (type == "game.input") {
|
||||
handleGameInput(ws, payload);
|
||||
} else {
|
||||
sendError(ws, "unknown_type", "unknown message type: " + type);
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
// payload может быть не тем типом, что ожидает хендлер (например,
|
||||
// payload.value<T>() на не-объекте/не том типе поля) — это кидает
|
||||
// json::type_error. Не даём одному кривому сообщению уронить процесс
|
||||
// со всеми активными играми.
|
||||
sendError(ws, "bad_payload", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Ник рассылается другим игрокам и рендерится на клиенте как текст — режем
|
||||
// длину и выкидываем управляющие/разметочные символы, чтобы кривой или
|
||||
// специально вредоносный ник от одного игрока не ломал UI остальных.
|
||||
std::string sanitizeNickname(std::string nickname) {
|
||||
constexpr size_t kMaxLen = 20;
|
||||
if (nickname.empty()) {
|
||||
return "anon";
|
||||
}
|
||||
std::string out;
|
||||
out.reserve(std::min(nickname.size(), kMaxLen));
|
||||
for (unsigned char c : nickname) {
|
||||
if (out.size() >= kMaxLen) {
|
||||
break;
|
||||
}
|
||||
if (c == '<' || c == '>' || c == '&' || c == '"' || c == '\'' || c < 0x20) {
|
||||
continue;
|
||||
}
|
||||
out.push_back(static_cast<char>(c));
|
||||
}
|
||||
return out.empty() ? "anon" : out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void Lobby::handleHello(WS *ws, const json &payload) {
|
||||
auto *data = ws->getUserData();
|
||||
if (data->player_id.empty()) {
|
||||
data->player_id = "player-" + std::to_string(next_player_id_++);
|
||||
}
|
||||
data->nickname = payload.value("nickname", "anon");
|
||||
data->nickname = sanitizeNickname(payload.value("nickname", "anon"));
|
||||
send(ws, "hello.ack", {{"player_id", data->player_id}});
|
||||
}
|
||||
|
||||
@@ -140,6 +172,10 @@ void Lobby::handleCreateRoom(WS *ws, const json &payload) {
|
||||
sendError(ws, "invalid_mode", "mode must be \"coop\" or \"pvp\"");
|
||||
return;
|
||||
}
|
||||
if (rooms_.size() >= kMaxRooms) {
|
||||
sendError(ws, "too_many_rooms", "server is at capacity, try again later");
|
||||
return;
|
||||
}
|
||||
|
||||
Room room;
|
||||
room.id = generateRoomId();
|
||||
|
||||
Reference in New Issue
Block a user