Firebase, как защитить числовые данные от манипуляции пользователями, например. оценка игры

Я разрабатываю многопользовательскую игру с Firebase. Очки игрока записываются в базе данных после каждой игры, а также поле playerTotalScore обновляется с учетом нового итога. Мой вопрос: возможно ли защитить поле playerTotalScore от произвольных манипуляций со стороны пользователя, используя только правила безопасности на основе firebase? Если да, то как?

Я долго просматривал информацию о безопасности на базе Firebase. Хотя я понимаю, что в правилах безопасности можно реализовать некоторую сложную логику (".write": "!data.exists()" число на определенную величину, например, эту суть, или сделать поле ".write": "!data.exists()" только для вставки (".write": "!data.exists()") в этом случае никакая информация, похоже, не поможет. Правил только приращения будет недостаточно, поскольку счет можно манипулировать, увеличивая его несколько раз. Только вставка, по-видимому, является опцией для totalScore, поскольку она обновляется после каждого игра.

Обновить

По просьбе Като, здесь приведен конкретный вариант использования.

Игра, которую я разрабатываю, - это игра-опрос, в которой игроки отвечают на вопросы, и результаты игроков отображаются в режиме реального времени.

В течение игры счет для этой конкретной игры обновляется после каждого вопроса следующим утверждением:

gameRef.child('players').child(UserId).child('score').set(gameScore)

После окончания игры totalScore (все сыгранные игры) для игрока рассчитывается как totalScore=totalScore+gameScore а затем общая оценка игроков обновляется в Firebase с использованием следующего утверждения:

leaderboardRef.child(userId).setWithPriority({userName:userName, totalScore:totalScore}, totalScore)

Обновление 2: структура данных в соответствии с просьбой Като

Вот конкретная структура, которая у меня сейчас есть. Это далеко не все, поэтому я готов изменить его так, как это необходимо, в соответствии с рекомендуемым подходом для защиты данных.

Счет за каждую игру, сыгранную пользователем (игроком), сохраняется в следующей структуре

<firebase_root>/app/games/<gameId>/players/<userId>/score/

<gameId> - это ключ, сгенерированный <gameId> в результате вызова метода push() firebase. <UserId> - это простой логин пользователя firebase.

TotalScore (сумма всех очков за все сыгранные игры) для каждого пользователя (игрока) хранится в следующей структуре данных

<firebase_root>/app/leaderboard/<userId>/totalScore/

данные списка лидеров для totalScore устанавливаются с использованием totalScore в качестве приоритета для запросов

leaderboardRef.child(userId).setWithPriority({userName:userName, totalScore:totalScore}, totalScore)

Score и totalScore являются числовыми целочисленными значениями. Это все детали текущей структуры данных, о которых я могу думать.

Ответ 1

Ваш вопрос технически заключается в том, как завершить это с помощью правил безопасности, но поскольку это немного проблема XY, и ни одна из других возможностей не была исключена, Я также займусь некоторыми из них.

Я буду делать много предположений, так как для ответа на этот вопрос на самом деле требуется полностью определенный набор правил, которым необходимо следовать, и на самом деле это вопрос внедрения целого приложения (увеличение балла является результатом правила логики игры, а не простая математическая проблема).

Всего баллов у клиента

Возможно, самый простой ответ на эту загадку - просто не иметь общий балл. Просто возьмите список игроков и суммируйте их вручную.

Если это может быть полезно:

  • список игроков составляет сотни или меньше.
  • данные игрока соответственно малы (не 500k каждый)

Как это сделать:

var ref = new Firebase(URL);
function getTotalScore(gameId, callback) {
   ref.child('app/games/' + gameId + '/players').once('value', function(playerListSnap) {
      var total = 0;
      playerListSnap.forEach(function(playerSnap) {
         var data = playerSnap.val();
         total += data.totalScore || 0;
      });
      callback(gameId, total);
   });
}

Использовать привилегированного работника для обновления оценки

Очень сложный и простой подход (поскольку он требует только того, чтобы правила безопасности были установлены как-то вроде ".write": "auth.uid === 'SERVER_PROCESS'"), было бы использовать серверный процесс, который просто контролирует игры и накапливает итоговые значения. Это, вероятно, самое простое решение для правильного и простого в обслуживании, но имеет недостаток в необходимости использования другой рабочей части.

Если это может быть полезно:

  • вы можете развернуть службу Heroku или развернуть файл .js на webscript.io
  • дополнительная ежемесячная подписка в диапазоне $5 $30 не является нарушителем транзакций

Как это сделать:

Очевидно, это связано с большим количеством дизайна приложения, и есть различные уровни, над которыми это должно быть достигнуто. Давайте сосредоточимся только на закрытии игр и подсчете лидеров, так как это хороший пример.

Начните с разбивки кода подсчета на свой собственный путь, например

/scores_entries/$gameid/$scoreid = < player: ..., score: ... >
/game_scores/$gameid/$playerid = <integer>

Теперь отслеживайте игры, чтобы увидеть, когда они закрываются:

var rootRef = new Firebase(URL);
var gamesRef = rootRef.child('app/games');
var lbRef = rootRef.child('leaderboards');

gamesRef.on('child_added', watchGame);
gamesRef.child('app/games').on('child_remove', unwatchGame);

function watchGame(snap) {
    snap.ref().child('status').on('value', gameStatusChanged);
}

function unwatchGame(snap) {
    snap.ref().child('status').off('value', gameStatusChanged);
}

function gameStatusChanged(snap) {
    if( snap.val() === 'CLOSED' ) {
        unwatchGame(snap);
        calculateScores(snap.name());
    }
}

function calculateScores(gameId) {
    gamesRef.child(gameId).child('users').once('value', function(snap) {
        var userScores = {};
        snap.forEach(function(ss) {
            var score = ss.val() || 0;
            userScores[ss.name()] = score;
        });
        updateLeaderboards(userScores);
    });
}

function updateLeaderboards(userScores) {
    for(var userId in userScores) {
        var score = userScores[userId];
        lbRef.child(userId).transaction(function(currentValue) {
            return (currentValue||0) + score;
        });
    }
}

Использовать путь аудита и правила безопасности

Это, конечно, будет самым сложным и трудным из доступных вариантов.

Если это может быть полезно:

  • когда мы отказываемся использовать любую другую стратегию, связанную с серверным процессом.
  • когда ужасно беспокоились об обманах игроков.
  • когда у нас есть много дополнительного времени для записи

Очевидно, я склонен к этому подходу. Прежде всего потому, что очень трудно получить право и требует много энергии, которая может быть заменена небольшими денежными вложениями.

Получение этого права требует тщательного изучения каждого индивидуального запроса на запись. Есть несколько очевидных точек для защиты (возможно, больше):

  • Написание любого игрового события, которое включает в себя приращение счета
  • Написание общей суммы для игры на пользователя
  • Написание общей суммы игры в таблице лидеров
  • Запись каждой записи аудита
  • Обеспечение лишних игр не может быть создано и изменено "на лету" только для увеличения баллов.

Вот некоторые основные основы для обеспечения каждой из этих точек:

  • используйте контрольный журнал, в котором пользователи могут добавлять (не обновлять или удалять) записи
  • подтвердите, что каждая запись аудита имеет приоритет, равный текущей временной отметке
  • подтвердите, что каждая запись аудита содержит достоверные данные в соответствии с текущим состоянием игры
  • использовать записи аудита при попытке увеличения итоговых итогов

Пусть, например, безопасно обновляет таблицу лидеров. Будем считать следующее:

  • рейтинг пользователя в игре действителен.
  • пользователь создал запись аудита, скажем, leaderboard_audit/$userid/$gameid, с текущей меткой времени в качестве приоритета и оценки в качестве значения
  • каждая запись пользователя существует в таблице лидеров
  • только пользователь может обновить свой собственный счет.

Итак, наша предполагаемая структура данных:

/games/$gameid/users/$userid/score
/leaderboard_audit/$userid/$gameid/score
/leaderboard/$userid = { last_game: $gameid, score: <int> }

Вот как работает наша логика:

  • оценка игры равна /games/$gameid/users/$userid/score
  • запись аудита создается в /leaderboard_audit/$userid/games_played/$gameid
  • значение в /leaderboard_audit/$userid/last_game обновляется в соответствии с $gameid
  • таблица лидеров обновляется на сумму, равную last_game записи аудита

И вот настоящие правила:

{
    "rules": {
        "leaderboard_audit": {
            "$userid": {
                "$gameid": {
                   // newData.exists() ensures records cannot be deleted
                    ".write": "auth.uid === $userid && newData.exists()",

                    ".validate": "
                        // can only create new records
                        !data.exists()
                        // references a valid game
                        && root.child('games/' + $gameid).exists()
                        // has the correct score as the value
                        && newData.val() === root.child('games/' + $gameid + '/users/' + auth.uid + '/score').val()
                        // has a priority equal to the current timestamp
                        && newData.getPriority() === now
                        // is created after the previous last_game or there isn't a last_game
                        (
                            !root.child('leaderboard/' + auth.uid + '/last_game').exists() || 
                            newData.getPriority() > data.parent().child(root.child('leaderboard/' + auth.uid + '/last_game').val()).getPriority()
                        )

                    "
                }
            }
        },
        "leaderboard": {
            "$userid": {
                ".write": "auth.uid === $userid && newData.exists()",
                ".validate": "newData.hasChildren(['last_game', 'score'])",
                "last_game": {
                    ".validate": "
                        // must match the last_game entry
                        newData.val() === root.child('leaderboard_audit/' + auth.uid + '/last_game').val()
                        // must not be a duplicate
                        newData.val() !== data.val()
                        // must be a game created after the current last_game timestamp
                        (
                            !data.exists() ||
                            root.child('leaderboard_audit/' + auth.uid + '/' + data.val()).getPriority() 
                            < root.child('leaderboard_audit/' + auth.uid + '/' + newData.val()).getPriority()
                        )
                    "
                },
                "score": {
                    ".validate": "
                        // new score is equal to the old score plus the last_game score
                        newData.val() === data.val() + 
                        root.child('games/' + newData.parent().child('last_game').val() + '/users/' + auth.uid + '/score').val()
                    "
                }
            }
        }
    }
}

Ответ 2

Будет сложно защищать от недопустимых значений с помощью правил. Поскольку вы даете права пользователя писать значение, они также могут перепроектировать код и написать значения, которые вы бы лучше не видели. Вы можете сделать много вещей, чтобы затруднить работу хакера, но всегда найдется кто-то, кто сможет обойти это. Это сказало: есть некоторые легкие вещи, которые вы можете сделать, чтобы сделать вещи для хакеров немного менее тривиальными.

Что-то, что вы можете легко сделать, это записать/хранить достаточно информации об игровом процессе, чтобы потом определить, является ли это законным.

Итак, например, в типизированной игре я не только сохранил окончательный результат для игрока, но и каждую клавишу, которую они нажали, и когда они нажали.

https://<my>.firebaseio.com/highscores/game_1_time_15/puf
  keystrokes: "[[747,'e'],[827,'i'],[971,'t'],[1036,'h']...[14880,'e']]"
  score: 61

Итак, на 747ms в игре я набрал e, затем i, t, h и т.д., пока, наконец, после 14.8 секунд я не нажал e.

Используя эти значения, я могу проверить, действительно ли нажатые клавиши приводят к значению 61. Я мог бы также воспроизвести игру или сделать некоторый анализ на ней, чтобы увидеть, кажется ли это, что настоящая человеческая игра нажала клавиши. Если отметки времени 100, 200, 300 и т.д., Вы были бы довольно подозрительными (хотя я создал несколько ботов, которые набирают точно с такими интервалами).

Конечно, это не гарантия, но это, по крайней мере, первый камень преткновения для хакеров ref.child('score').set(10000000).

Я получил эту идею от Джона Ресига Дипа Липа, но я не могу найти страницу, где он ее описывает.

Ответ 3

У меня есть идея. - так как это многопользовательская игра, у вас будет несколько игроков в одной конкретной игре. это означает, что каждый из игроков после сообщения game over будет обновлять частичный и общий балл.

В правилах безопасности вы можете проверить, написало ли противнику частичное значение той же игры. - это будет доступ только для чтения. Или вы можете проверить, дают ли все частичные значения противникам требуемое общее количество и т.д.

Хакеру придется придумать какой-то подробный план, включающий управление несколькими учетными записями и синхронизацию атаки.

изменить: ... и я могу видеть дальнейший вопрос - а как насчет первого игрока для обновления? Это можно сделать по существу. Итак, во-первых, все игроки пишут intent to write score, где будет частичный балл, и когда есть какие-то значения во всем мире, они будут понятны, чтобы написать фактический балл.