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:
@@ -45,6 +45,21 @@ Game::Game(Room &room, std::function<void()> onFinished)
|
||||
}
|
||||
tanks_.push_back(t);
|
||||
}
|
||||
|
||||
// На coop-карте укрытие у базы (в т.ч. кирпичная стена над ней) кладётся
|
||||
// фиксированным блоком независимо от того, что уже занято, и один из
|
||||
// вариантов раскладки перекрывал стартовый тайл игрока — танк рождался
|
||||
// внутри кирпича и не мог выехать (rectHitsSolid блокировал любой шаг,
|
||||
// т.к. новая позиция всё ещё пересекала тот же тайл). Расчищаем тайл под
|
||||
// каждым стартовым танком уже после того, как вся карта построена, чтобы
|
||||
// спавн был гарантированно проходим при любой раскладке.
|
||||
for (const auto &t : tanks_) {
|
||||
int tx = (int)std::floor(t.x);
|
||||
int ty = (int)std::floor(t.y);
|
||||
if (ty >= 0 && ty < kMapHeight && tx >= 0 && tx < kMapWidth) {
|
||||
map_[ty][tx] = kEmpty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Game::~Game() {
|
||||
|
||||
+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();
|
||||
|
||||
@@ -46,6 +46,9 @@ public:
|
||||
|
||||
private:
|
||||
static constexpr size_t kMaxPlayersPerRoom = 2;
|
||||
// Верхняя граница на общее число комнат — без неё клиент, создающий и не
|
||||
// покидающий комнаты, мог бы разогнать rooms_ до исчерпания памяти.
|
||||
static constexpr size_t kMaxRooms = 200;
|
||||
|
||||
std::unordered_map<std::string, Room> rooms_;
|
||||
std::unordered_map<std::string, std::unique_ptr<Game>> games_;
|
||||
|
||||
Reference in New Issue
Block a user