安全规则中的 Firebase 速率限制?

Posted

技术标签:

【中文标题】安全规则中的 Firebase 速率限制?【英文标题】:Firebase rate limiting in security rules? 【发布时间】:2021-07-31 21:36:30 【问题描述】:

我启动了我的第一个开放存储库项目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】:

诀窍是对用户上次发布消息的时间进行审核。然后您可以根据审核值强制发布每条消息的时间:


  "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 
          
        
       
  

看到它在行动in this fiddle。这是小提琴的要点:

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);
            
        );
    )

【讨论】:

这对仁慈的客户来说非常棒。黑客攻击怎么办?有人不能发布到 last_message 参考。然后,他们可以敲定你的推荐信,一遍又一遍地填写请求。有没有办法提供速率限制来避免这种情况? 实际上,我想我收回了这一点。加藤似乎已经涵盖了这一点。您必须发布到 last_message 或写入“消息”将失败。 “last_message”通过要求最后一条消息不少于 5 秒前来防止填塞。很优雅 是的,不是最简单的规则来包裹大脑,但它们确实可以完成工作! 有没有办法让未经身份验证的用户使用它?我的猜测是 做这件事并不重要,所以 guid 可能是第一个表中的关键而不是 $user。不确定当前的实现是否这样做,但您可能还必须限制重新发送,并可能为每条消息设置 a 和 min 时间窗口。你觉得这听起来对吗?不过,我想问题在于,您可以将消息聚集在一起以通过速率限制... 你想太多了,这太复杂了,你不会遇到一次性的。如果您的应用程序需要银行级别的安全性,请编写一个服务器端进程并让它推送消息,添加您想要的任何级别的限制。或者节省一些时间,让您的应用发布并投入使用。【参考方案2】:

我没有足够的声誉来写评论,但我同意 Victor 的评论。如果您将 fb.child('messages').push(...) 插入循环(即 for (let i = 0; i &lt; 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 代码与 Kato 的解决方案非常相似,只是 getTimestamp 将返回 time: number, key: string 到下一个回调。然后我们只需要ref.update([key]: data)

这个方案避免了 500ms 的时间窗口,我们不必担心客户端必须足够快才能在 500ms 内推送消息。如果发送多个写入请求(垃圾邮件),它们只能写入messages 中的 1 个单个键。或者,messages 中的仅创建规则可以防止这种情况发生。

【讨论】:

【参考方案3】:

现有答案使用两个数据库更新:(1) 标记时间戳,(2) 将标记的时间戳附加到实际写入。 Kato's answer 需要 500 毫秒的时间窗口,而 ChiNhan 需要记住下一个键。

有一种更简单的方法可以在单个数据库更新中完成。这个想法是使用update() 方法一次将多个值写入数据库。安全规则验证写入的值,以便写入不会超过配额。配额定义为一对值:quotaTimestamppostCountpostCount 是在 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);

将速率限制为每分钟最多 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 以避免时钟偏差。

【讨论】:

【参考方案4】:

我喜欢Kato's answer,但它没有考虑到恶意用户仅使用 for 循环就在 500 毫秒窗口之间充斥着聊天。 我提出了消除这种可能性的变体:


  "rules": 
    "users": 
      "$uid": 
        "rateLimit": 
          "lastMessage": 
            // newData.exists() ensures newData is not null and prevents deleting node
            // and $uid === auth.uid ensures the user writing this child node is the owner
            ".write": "newData.exists() && $uid === auth.uid",
            // newData.val() === now ensures the value written is the current timestamp
            // to avoid tricking the rules writting false values
            // and (!data.exists() || newData.val() > data.val() + 5000)
            // ensures no data exists currently in the node. Otherwise it checks if the
            // data that will overwrite the node is a value higher than the current timestamp
            // plus the value that will rate limit our messages expressed in milliseconds.
            // In this case a value of 5000 means that we can only send a message if
            // the last message we sent was more than 5 seconds ago
            ".validate": "newData.val() === now && (!data.exists() || newData.val() > data.val() + 5000)"
          
        
      
    ,
    "messages": 
      "$messageId": 
        // This rule ensures that we write lastMessage node avoiding just sending the message without
        // registering a new timestamp
        ".write": "newData.parent().parent().child('users').child(auth.uid).child('rateLimit').child('lastMessage').val() === now",
        // This rule ensures that we have all the required message fields
        ".validate": "newData.hasChildren(['timestamp', 'uid', 'message'])",
        "uid": 
          // This rule ensures that the value written is the id of the message sender
          ".validate": "newData.val() === auth.uid"
        ,
        "timestamp": 
          // This rule ensures that the message timestamp can't be modified
          ".write": "!data.exists()",
          // This rule ensures that the value written is the current timestamp
          ".validate": "newData.val() === now"
        ,
        "message": 
          // This rule ensures that the value written is a string
          ".validate": "newData.isString()"
        ,
        "$other": 
          // This rule ensures that we cant write other fields in the message other than
         // the explicitly declared above
         ".validate": false
        
      
    
  

代码实现使用跨多个位置的原子写入。如果一次验证失败,则操作未完成,数据库中不进行任何操作

function sendMessage(message) 
    const database = firebase.database();

    const pushId = database.ref().child("messages").push().key;
    const userId = firebase.auth().currentUser.uid;
    const timestampPlaceholder = firebase.database.ServerValue.TIMESTAMP;

    let updates = ;
    updates["messages/" + pushId] = 
      uid: userId,
      timestamp: timestampPlaceholder,
      message: message,
    ;
    updates[`users/$userId/rateLimit/lastMessage`] = timestampPlaceholder;

    database.ref().update(updates);
  

【讨论】:

以上是关于安全规则中的 Firebase 速率限制?的主要内容,如果未能解决你的问题,请参考以下文章

如何限制从 Firebase 读取数据的速率?

Firebase存储安全规则为特定用户提供访问权限

Firebase 安全规则:限制仅写入此 uid,几个字段除外

Fire Storage Exception([firebase_storage/unauthorized] 用户无权执行所需的操作。)(我的规则是允许读/写)

node_modules/@angular/fire/firebase.app.module.d.ts 中的错误?

我们如何为 Firebase 存储编写依赖于 Firebase 实时数据库中的值的安全规则? [复制]