web-dev-qa-db-ja.com

セキュリティルールでのFirebaseレート制限?

私は最初のオープンリポジトリプロジェクト EphChat を立ち上げ、人々はすぐにそれを要求で溢れさせました。

Firebaseには、セキュリティルールでリクエストをレート制限する方法がありますか?リクエストの時間と以前に書き込まれたデータの時間を使用してそれを行う方法があると思いますが、これを行う方法に関するドキュメントには何も見つかりません。

現在のセキュリティルールは次のとおりです。

{
    "rules": {
      "rooms": {
        "$RoomId": {
          "connections": {
              ".read": true,
              ".write": "auth.username == newData.child('FBUserId').val()"
          },
          "messages": {
            "$any": {
            ".write": "!newData.exists() || root.child('rooms').child(newData.child('RoomId').val()).child('connections').hasChild(newData.child('FBUserId').val())",
            ".validate": "newData.hasChildren(['RoomId','FBUserId','userName','userId','message']) && newData.child('message').val().length >= 1",
            ".read": "root.child('rooms').child(data.child('RoomId').val()).child('connections').hasChild(data.child('FBUserId').val())"
            }
          },
          "poll": {
            ".write": "auth.username == newData.child('FBUserId').val()",
            ".read": true
          }
        }
      }
    }
}

Roomsオブジェクト全体のデータベースへの書き込み(および読み取り?)をレート制限したいので、(たとえば)1秒あたり1つのリクエストしか実行できません。

ありがとう!

31
Brian Mayer

秘訣は、ユーザーが最後にメッセージを投稿したときの監査を維持することです。次に、監査値に基づいて各メッセージが投稿される時間を強制できます。

{
  "rules": {
          // this stores the last message I sent so I can throttle them by timestamp
      "last_message": {
        "$user": {
          // timestamp can't be deleted or I could just recreate it to bypass our throttle
          ".write": "newData.exists() && auth.uid === $user",
          // the new value must be at least 5000 milliseconds after the last (no more than one message every five seconds)
          // the new value must be before now (it will be since `now` is when it reaches the server unless I try to cheat)
          ".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val()+5000)"
        }
      },

      "messages": {
        "$message_id": {
          // message must have a timestamp attribute and a sender attribute
          ".write": "newData.hasChildren(['timestamp', 'sender', 'message'])",
          "sender": {
            ".validate": "newData.val() === auth.uid"
          },
          "timestamp": {
            // in order to write a message, I must first make an entry in timestamp_index
            // additionally, that message must be within 500ms of now, which means I can't
            // just re-use the same one over and over, thus, we've effectively required messages
            // to be 5 seconds apart
            ".validate": "newData.val() >= now - 500 && newData.val() === data.parent().parent().parent().child('last_message/'+auth.uid).val()"
          },
          "message": {
            ".validate": "newData.isString() && newData.val().length < 500" 
          },
          "$other": {
            ".validate": false 
          }
        }
      } 
  }
}

実際にご覧ください このフィドルで 。フィドルの内容の要点は次のとおりです。

var fb = new Firebase(URL);
var userId; // log in and store user.uid here

// run our create routine
createRecord(data, function (recordId, timestamp) {
   console.log('created record ' + recordId + ' at time ' + new Date(timestamp));
});

// updates the last_message/ path and returns the current timestamp
function getTimestamp(next) {
    var ref = fb.child('last_message/' + userId);
    ref.set(Firebase.ServerValue.TIMESTAMP, function (err) {
        if (err) { console.error(err); }
        else {
            ref.once('value', function (snap) {
                next(snap.val());
            });
        }
    });
}

function createRecord(data, next) {
    getTimestamp(function (timestamp) {
        // add the new timestamp to the record data
        var data = {
          sender: userId,
          timestamp: timestamp,
          message: 'hello world'
        };

        var ref = fb.child('messages').Push(data, function (err) {
            if (err) { console.error(err); }
            else {
               next(ref.name(), timestamp);
            }
        });
    })
}
41
Kato

私はコメントを書くのに十分な評判はありませんが、ビクターのコメントに同意します。 fb.child('messages').Push(...)をループに挿入すると(つまり、for (let i = 0; i < 100; i++) {...})、60〜80のメッセージを(500ミリ秒のウィンドウフレームに)プッシュします。

加藤のソリューションに触発されて、私は以下のようにルールの修正を提案します:

_rules: {
  users: {
    "$uid": {
      "timestamp": { // similar to Kato's answer
        ".write": "auth.uid === $uid && newData.exists()"
        ,".read": "auth.uid === $uid"
        ,".validate": "newData.hasChildren(['time', 'key'])"
        ,"time": {
          ".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val() + 1000)"
        }
        ,"key": {

        }
      }
      ,"messages": {
        "$key": { /// this key has to be the same is the key in timestamp (checked by .validate)
           ".write": "auth.uid === $uid && !data.exists()" ///only 'create' allow
           ,".validate": "newData.hasChildren(['message']) && $key === root.child('/users/' + $uid + '/timestamp/key').val()"
           ,"message": { ".validate": "newData.isString()" }
           /// ...and any other datas such as 'time', 'to'....
        }
      }
    }
  }
}
_

.jsコードは、getTimestampが次のコールバックに{time:number、key:string}を返すことを除いて、Katoのソリューションに非常に似ています。次に、ref.update({[key]: data})を実行する必要があります

このソリューションは500ミリ秒の時間ウィンドウを回避します。クライアントが500ミリ秒以内にメッセージをプッシュするのに十分な速さである必要があることを心配する必要はありません。複数の書き込みリクエストが送信された場合(スパミング)、それらはmessagesの単一のキーにのみ書き込むことができます。必要に応じて、messagesの作成のみのルールにより、それが発生しないようにします。

2
ChiNhan

既存の回答は2つのデータベース更新を使用します。(1)タイムスタンプをマークし、(2)マークしたタイムスタンプを実際の書き込みに添付します。加藤の答えには500msの時間ウィンドウが必要ですが、ChiNhanの答えには次のキーを覚えておく必要があります。

単一のデータベース更新でそれを行う簡単な方法があります。アイデアは、 pdate() メソッドを使用して、一度に複数の値をデータベースに書き込むことです。セキュリティルールは、書き込まれた値を検証して、書き込みが割り当てを超えないようにします。クォータは、quotaTimestampおよびpostCountの値のペアとして定義されます。 postCountはquotaTimestampから1分以内に書き込まれた投稿の数です。セキュリティルールは、postCountが特定の値を超えると、次の書き込みを拒否します。 quotaTimestampが1分より古い場合、postCountはリセットされます。

新しいメッセージを投稿する方法は次のとおりです。

function postMessage(user, message) {
  const now = Date.now() + serverTimeOffset;
  if (!user.quotaTimestamp || user.quotaTimestamp + 60 * 1000 < now) {
    // Resets the quota when 1 minute has elapsed since the quotaTimestamp.
    user.quotaTimestamp = database.ServerValue.TIMESTAMP;
    user.postCount = 0;
  }
  user.postCount++;

  const values = {};
  const messageId = // generate unique id
  values[`users/${user.uid}/quotaTimestamp`] = user.quotaTimestamp;
  values[`users/${user.uid}/postCount`] = user.postCount;
  values[`messages/${messageId}`] = {
    sender: ...,
    message: ...,
    ...
  };
  return this.db.database.ref().update(values);
}

1分あたり最大5件の投稿にレート制限するセキュリティルール:

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "$uid === auth.uid",
        ".write": "$uid === auth.uid && newData.child('postCount').val() <= 5",
        "quotaTimestamp": {
          // Only allow updating quotaTimestamp if it's staler than 1 minute.
          ".validate": "
            newData.isNumber()
            && (newData.val() === now
              ? (data.val() + 60 * 1000 < now)
              : (data.val() == newData.val()))"
        },
        "postCount": {
          // Only allow postCount to be incremented by 1
          // or reset to 1 when the quotaTimestamp is being refreshed.
          ".validate": "
            newData.isNumber()
            && (data.exists()
              ? (data.val() + 1 === newData.val()
                || (newData.val() === 1
                    && newData.parent().child('quotaTimestamp').val() === now))
              : (newData.val() === 1))"
        },
        "$other": { ".validate": false }
      }
    },

    "messages": {
      ...
    }
  }
}

注: serverTimeOffset を維持して、クロックスキューを回避する必要があります。

0
Felix Halim