65.9K
CodeProject 正在变化。 阅读更多。
Home

保护 Web 应用程序中的 ID

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (1投票)

2018年11月13日

CPOL

6分钟阅读

viewsIcon

9024

如何保护 Web 应用程序中的 ID。

引言

最近,我一直在阅读有关如何保护我们的 Web 应用程序免受篡改的文档和文章,当时正在将表单提供给用户。看看这个,人们会意识到这些安全措施的原因是由于 HTTP 的固有性质,即它是无状态的,我们必须找到解决这个问题的方法。

在我们开始之前,我想简要介绍一下 ASP .NET 应用程序的默认脚手架中存在的标准实践,尽管它们在其他平台上的使用也是一种好的实践。接下来,我们将讨论本文的主要话题,思考一下,这可能有点矫枉过正,也可能不是;无论如何,这确实是一个风险。

陈旧的路径

为了获得一些实践经验,在本文的其余部分,我们将使用一个新的 ASP .NET Core 应用程序(不一定非得是 .NET Core,.NET Framework 也可以)。

在此应用程序中,我们将使用以下模型类

    using System;
    public class Student
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime EnrollementDate { get; set; }
        public int Credits { get; set; }
    }

到目前为止,它只是我们一直看到的普通模型类,一个 ID 和一组属性。

现在,有了这个模型,我们将创建一个新的控制器,为该模型生成脚手架,并检查生成的动作和视图,以及实现的安全性措施。

[HttpPost]
[ValidateAntiForgeryToken]
public async Task Create([Bind("Id,FirstName,LastName,EnrollementDate,Credits")] Student student)
[HttpPost]
[ValidateAntiForgeryToken]
public async Task Edit(int id, [Bind("Id,FirstName,LastName,EnrollementDate,Credits")] Student student)

在这里,我们可以看到以下两种最佳实践;我们将逐一介绍并添加一些补充说明

  • [ValidateAntiForgeryToken] 属性 – 这有助于我们防范跨站脚本攻击,这意味着当页面创建时,会为该页面生成一个唯一的令牌,因此当页面发布时,我们将知道它是否来自我们的页面。这样,我们就阻止了其他人从其他网站(如网络钓鱼攻击)向我们的网站发布信息。
  • [Bind(“…”)] 属性 – 这是工作流程的一个极好的补充,它有助于我们处理过量发布。要理解过量发布,首先,我们必须认识到 MVC 框架使用一个称为**模型绑定器**的构造,它接收发布信息并将其转换为一个实际的 Student 对象,并填入所有字段。正因为如此,如果我们不小心,有人可能会拦截或修改他们浏览器中的页面,例如,他们可以覆盖表单中根本没有暴露的字段。例如,如果 Credits 属性不在表单页面上,但有人仍然以填入该字段的方式发送了它,那么通过模型绑定创建的 Student 对象也将填入该属性。因此,使用 [Bind] 属性,我们可以明确地告诉模型我们想要查找哪些字段/属性,并忽略其他字段。

不过,我个人认为 [Bind] 属性有一个更好的替代方案,它也与我们要做的更改非常契合,那就是使用视图模型或数据传输对象(尽管这种方法需要额外的基础架构来手动分配字段或使用 AutoMapper 等框架),例如,如果我们想隐藏 Credits 属性,我们将得到以下结果

public class StudentViewModel
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime EnrollementDate { get; set; }
}

正如我们在视图模型中看到的,如果没有 Credits 字段,则模型绑定器无法填充它。

ID 呢?我们应该关心吗?

好吧,正如我们所见,过量发布会造成真正的问题,尤其对于编辑/更新表单来说,缺点是 ID 没有安全检查。

var student = await _context.Student.SingleOrDefaultAsync(m => m.Id == id);

上面的这些行很常见,当我们谈论 ID 时,我们只关心系统中有一个 ID。但如果有人想捣乱呢?

警告:以下场景是虚构的,但仍然不应该被采纳,它们仅作为例子提出以提高意识,请不要更改您无权更改的系统信息。

这是一个可能发生的场景

在我们的系统中,只有工作人员可以更新学生的学分,所以他们需要被授权,但即使被允许,一个特定的老师可能只能接触到他们有课的学生。所以,假设老师可以访问该系统,但无法访问某个特定学生,那么,除非除了查看页面(也就是仅客户端验证)之外还有其他保障措施,否则该人可能会更改他们学生的表单信息,但将 ID 更改为不在他们班级中的某人。

另一个类似的例子是,在一个集中的电子商务平台上,某人只能更改他们所在位置的产品信息,但通过使用这种方法,他们也可以更改其他地方的数据。

同样,这发生是因为我们必须将标识符保存在页面或 URL 的某个地方,因为 HTTP 是无状态的。

那么,我们能做些什么来帮助解决这种情况呢?嗯,凭直觉,我想到了两种解决方案。

  1. 开始使用 GUID(全局唯一标识符)而不是整数作为 ID。我个人总是使用 GUID,因为与整数相比,它们有几个优点。
    1. GUID 可以在离线生成,无需先将其保存到数据库,且发生冲突的几率很小。
    2. 在数据库中工作时,整数 ID 可能会在发生错误时跳过几个数字,这会导致编号系统出现漏洞,如果有一个发票系统,那么修复起来会非常麻烦,尤其是当几周后才发现时。

    3. GUID 在生成时是不可预测的(在它们可能的情况下),这意味着在我们之前的老师-学生场景中,除非进行修改的人明确知道 ID,否则他们猜到 ID 的几率很小。

  2. 添加一个控制变量来检查该表单的发布是否确实是针对该对象的。你可以添加一个页面上包含的 ID 的哈希值作为隐藏输入,当发布时,检查它们是否仍然匹配,这样,即使有人事先知道 ID,进行任何损害也将更加困难。

现在让我们看一个这种哈希的例子,一个非常简单的但仍然可行的例子

public class StudentViewModel
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime EnrollementDate { get; set; }

    public string SecretHash => (Id ^ 5 + 7).ToString();
}

在这里,我们添加了一个 getter,它将根据 ID 值返回一个 string;这个操作可以像您希望的那么复杂。接下来,我们将更新编辑视图

<div class="row">
    <div class="col-md-4">

            <div class="text-danger"></div>            
            
            <div class="form-group">

                <span class="text-danger"></span>
            </div>
            <div class="form-group">

                <span class="text-danger"></span>
            </div>
            <div class="form-group">

                <span class="text-danger"></span>
            </div>
            <div class="form-group">

                <span class="text-danger"></span>
            </div>
            <div class="form-group">
            </div>        
    </div>
</div>

现在,每当此表单发布时,SecretHash 将随之发送。最后,我们将更新 Edit 动作方法

[HttpPost]
[ValidateAntiForgeryToken]
public async Task Edit(int id, StudentViewModel studentViewModel)
{
    var student = await _context.Student.SingleOrDefaultAsync(m => m.Id == id);

    var checkViewModel = new StudentViewModel { Id = student.Id };
    if (studentViewModel.SecretHash != checkViewModel.SecretHash)
    {
        // Do something about it
    }
}

无疑有更简洁的处理方法,但关键在于 ID 与所有其他数据一样重要,并且请记住,由于我们同时暴露了 IdSecretHash(使用其他不那么明显的名称)。

结论

我们需要如此复杂的 ID 系统吗?答案是“我不知道,你能承担冒险的风险吗?” 引人深思… 🙂

希望您喜欢这个例子,即使对我来说,有时听起来有点偏执,但无论如何,如果可以做到,总有一天会做到的。

© . All rights reserved.