【问题标题】:How to prevent collection modification via console for an otherwise secure update operation?如何防止通过控制台修改集合以进行其他安全更新操作?
【发布时间】:2014-09-19 13:24:10
【问题描述】:

我对 Meteor 还很陌生,我只是想弄清楚流星的安全性。

我正在编写一个测验应用程序,允许登录用户保存他们的分数。我创建了一个由用户 ID 和分数数组组成的集合。我公开推送新分数的方式是服务器端的一种方法:

Meteor.methods({ 
  'pushScore' : function(playerId, playerScore) {
    UserScores.upsert({ userId : playerId}, {$push : {scores : playerScore}});
  }
});

我在点击客户端的按钮时调用该方法,如下所示:

if (Meteor.userId()){
  Meteor.call('pushScore', Meteor.userId(), Session.get("score"));
}

我有以下顾虑:

  • 显然用户可以在“会话”中操纵分数值并欺骗系统。有什么替代的安全机制可以在进行测验时跟踪运行分数?
  • 另一个可能是更大的问题。如何防止用户只是向我的方法“pushScore”发出控制台调用,并通过添加例如 100 分来再次欺骗系统?
  • 我在这里的设计方式是否存在固有缺陷?

这只是一个示例应用程序,但我可以很容易地想象一个可以模仿这个的真实世界场景。在这种情况下,最佳做法是什么?

提前致谢。

干杯..

【问题讨论】:

    标签: meteor


    【解决方案1】:

    正如@Peppe 建议的那样,您应该以某种方式将逻辑移至服务器。 Meteor 安全(以及一般的网络安全)的主要规则是

    你不能信任客户。

    原因就是您已经提到的:如果客户端可以做某事,那么就无法阻止流氓用户从浏览器控制台做同样的事情,甚至无法自己编写将利用泄漏的恶意客户端。

    在您的情况下,这意味着如果客户能够为分数加分,那么无论您采用何种安全措施,用户也可以这样做。您可以使这或多或少变得困难,但您的系统有一个设计泄漏,无法完全关闭。

    因此,唯一的防弹解决方案是让服务器决定何时分配积分。我假设在测验应用程序中,用户选择正确答案时会获得分数。因此,与其在客户端检查,不如创建一个服务器端方法,该方法将接收问题 ID、答案 ID,并在答案正确时增加用户分数。然后确保用户不能只使用与您的测验设计相对应的所有可能答案来调用此方法 - 例如,如果选择错误答案,则给予负分,或者允许在一段时间内只回答相同的问题一次。

    最后,确保客户端不仅仅在接收到的数据中获得正确的答案 ID。

    【讨论】:

    • 谢谢。这种解释是有道理的。让我重新设计我的测验应用程序,牢记这些要点,我将尝试在此处发布我重构的代码。
    • 我不同意,当客户端可以与自己的mongo客户端交互并让mongodbs oplog承担其余部分时,为什么要向服务器发送数据以便它可以向mongo发送数据?我相信强大的“Meteor.allow”验证并完全信任客户端,我相信通过 mongo 与数据交互是“Meteor Way”
    • @ErezHochman:我们说的是同一件事。 Meteor.allow 中的验证意味着服务器运行逻辑来决定是否可以进行更新,这是我的观点。我并没有说你不应该在客户端上插入/更新并且只依赖方法。只是在这种特殊情况下,您需要更新中未包含的信息(您仅更新分值,但需要有关问题和答案的信息)。
    • 因此在这种特殊情况下,我认为创建一个方法更简单,以便可以轻松传输这些附加信息。如果在您的架构中,您可以访问做出决定所需的所有信息——例如,如果您想将问题 ID 与分数事件一起包含在内——那么在客户端上更新并在允许中进行验证会更简单。尽管如此,服务器会做出决定,这意味着客户端不会被“信任”。
    • 哦,现在我明白了,是的,我同意,如果您需要使用方法来丰富您的数据,那么您可以这样做。
    【解决方案2】:

    简而言之,您的问题有两种常见的解决方案:

    1. 如果您使用 Meteor.method,则不要在 Meteor.call 中传递任何参数,服务器可以并且应该收集它计划在服务器端插入/更新的数据。

      李>
    2. 您可以使用集合“允许”方法向集合添加验证功能,以验证来自客户端的任何更新,在这种情况下,您不需要 Meteor.method,只需从客户端更新即可在服务器端验证它。

    【讨论】:

    • 并确保删除“不安全”包。
    • 我想我理解建议的方法。但我没有看到这些解决我的具体情况。可能是我错过了一些东西。就我而言,这是一个经过身份验证的用户。因此,他们有权添加新的测验分数。由于我没有跟踪集合中测验的完成情况,我是否可以针对服务器更新代码上的客户端操作进行验证以标记测验的完成?例如。在我允许服务器端更新方法上的新分数推送之前,我可以检查是否在 UI 上单击了提交测验按钮?
    • 感谢您的洞察力。
    【解决方案3】:

    meteor 中的安全性(插入/更新/删除操作)的工作方式与任何其他框架中的安全性相同:在执行用户执行的操作之前,请确保用户有权执行该操作。安全性在 Meteor 中可能看起来是一个弱点,但它并没有比其他框架更受它的影响(尽管在 Meteor 中通过控制台更容易利用它)。

    解决问题的最佳方法可能因情况而异,但这里有一个示例:如果用户发布帖子,则该用户应获得 5 分。这是一个不好的解决方法:

    if(Meteor.isClient){
    
        // Insert the post and increase points.
        Posts.insert({userId: Meteor.userId(), post: "The post."})
        Meteor.users.update(Meteor.userId(), {$inc: {'profile.points': 5}})
    
    }
    
    if(Meteor.isServer){
    
        Posts.allow({
            insert: function(userId, doc){
    
                check(doc, {
                    _id: String,
                    userId: String,
                    post: String
                })
    
                // You must be yourself.
                if(doc.userId != userId){
                    return false
                }
    
                return true
    
            }
        })
    
        Meteor.users.allow({
            update: function(userId, doc, fieldNames, modifier){
    
                check(modifier, {
                    $inc: {
                        'profile.points': Number
                    }
                })
    
                if(modifier.$inc['profile.points'] != 5){
                    return false
                }
    
                return true
    
            }
        })
    
    }
    

    是什么让它变得糟糕?用户可以在不发布帖子的情况下增加他的积分。这是一个更好的解决方案:

    if(Meteor.isClient){
    
        // Insert the post and increase points.
        Method.call('postAndIncrease', {userId: Meteor.userId(), post: "The post."})
    
    }
    
    if(Meteor.isServer){
    
        Meteor.methods({
            postAndIncrease: function(post){
    
                check(post, {
                    userId: String,
                    post: String
                })
    
                // You must be yourself.
                if(post.userId != this.userId){
                    return false
                }
    
                Posts.insert(post)
                Meteor.users.update(this.userId, {$inc: {'profile.points': 5}})
    
            }
        })
    
    }
    

    更好,但仍然很糟糕。为什么?由于延迟(帖子是在服务器上创建的,而不是在客户端上创建的)。这是一个更好的解决方案:

    if(Meteor.isClient){
    
        // Insert the post and increase points.
        Posts.insert({userId: Meteor.userId(), post: "The post."})
    
    }
    
    if(Meteor.isServer){
    
        Posts.allow({
            insert: function(userId, doc){
    
                check(doc, {
                    _id: String,
                    userId: String,
                    post: String
                })
    
                // You must be yourself.
                if(doc.userId != userId){
                    return false
                }
    
                return true
    
            }
        })
    
        Posts.find().observe({
            added: function(post){
    
                // When new posts are added, the user gain the points.
                Meteor.users.update(post.userId, {$inc: {'profile.points': 5}})
    
            }
        })
    
    }
    

    此解决方案的唯一缺点是积分增量的延迟,但这是我们必须忍受的(至少目前是这样)。在服务器上使用观察也可能是一个缺点,但我认为你可以通过使用包收集钩子来传递它。

    【讨论】:

    • 我认为第二种方式(使用 Meteor.methods)可能是最简单的,如果帖子创建延迟是这样一个问题,您可以考虑为您的 meteor.method 添加一个存根并处理 UX,这样用户不会感觉到。
    • 事实上,在现实生活中,第三种方式往往更简单。从长远来看,普通的收集操作更容易维护。为了使代码更清晰,您可以使用 matb33:collection-hooks 包而不是观察者来编写插入后挂钩。
    • 谢谢@Peppe!我将尝试将您的一些建议纳入我的重构中
    • a Meteor.method that does the same thing for both the server and the client 怎么样?在客户端,它将充当存根。
    • @DanDascalescu,是的,应该可以工作(正如 Erez 提到的),但是您需要在方法和允许/拒绝规则中验证帖子。这种验证当然可以分解为一个单独的函数,这样您就不需要复制代码,但它会使您的代码更加复杂。猜猜它的工作方式取决于您。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-02
    • 2013-06-18
    • 1970-01-01
    • 2016-03-22
    • 2017-01-24
    相关资源
    最近更新 更多