2024年最佳天气API
Django破碎的对象级授权指南:示例与预防
近年来,软件应用程序变得更加强大,可以进行更复杂的数据查询和访问。考虑到这一点,易受攻击的应用程序数量也在增加。一些臭名昭著的安全事件已经发生。一个例子是导致敏感数据泄露的T-Mobile电话号码查询。Facebook和Uber也经历了相当多的对象级安全漏洞。大、中、小公司都面临着可能对业务产生负面影响的安全攻击的风险。
在这篇文章中,我们将定义损坏的对象级授权。我们将提供一些实例并讨论一些可用于使我们的 Django 应用程序更安全的方法。希望您能够将所学知识应用到您的 Django 项目中。
对象级授权失效
对象级授权是一种安全或访问控制机制,它根据经过身份验证的用户尝试访问的内容或资源实例检查其权限。
这种类型的控制通常发生在尝试对资源中的特定对象执行操作时。例如,假设我们有一个用户帐户资源,并且想要访问具有唯一 ID 4 的单个用户帐户详细信息。给定示例中的访问控制通常在对象级别。
在大多数情况下,用户可以登录并访问用户帐户;但是,可以查看的帐户类型是有限的。例如,只有帐户所有者才能访问自己的数据,或者管理员可以访问所有用户数据。
对象级授权可以更精细地控制用户对每个资源对象可以采取的操作。对象是应用程序有权访问的任何信息。
对象级授权失效可描述为对象级资源上的任何安全漏洞。由于对象级授权允许更细粒度的权限,因此任何具有对象级授权漏洞的应用程序都有可能将敏感信息暴露给攻击者。
基于 API 的应用最容易受到对象级授权攻击。这是因为 API 旨在促进对对象的不同操作,例如创建、读取、更新和删除。如果围绕这些对象的授权被破坏,我们就面临着对象级授权漏洞的风险。
对象级授权失效的示例
以下场景说明了此类攻击的一个示例。假设我们在请求标头中有一个 API 端点www.example.com/api/messages/<ID> /。攻击者可以将 ID 信息修改为其他内容并获取数据访问权限。
例如,攻击者可以手动将 URL 中的 ID 修改为其他内容,例如67。这样,如果数据库中存在给定 ID, www.example.com/ api/messages/67/ 就可以显示该 ID 的消息详细信息。部分消息详细信息可能是发送者、接收者和内容。
另一种情况是,如果POST或PUT请求包含可更改或放入请求正文中的唯一 ID,攻击者可能会使用该实例更改资源对象的标识符。例如,他们可能会将以下 JSON 对象请求更改为www.example.com/api/contacts/:
{
"user": 23,
"contact": {
"full_name": "Chike Ada",
"email": "test@example.com"
},
"reference": "Adding a new contact to account"
}
还请考虑以下情况:可预测且结构不良的 API 资源对象 ID。使用自动递增的数字(例如 1…2…3),攻击者可以轻松找出 API 结构并操纵这些资源对象。例如,经过身份验证的攻击者可以将 ID 为 23 的用户的数据更新为其他内容,如下所示。
{
"user": 23,
"contact": {
"full_name": "Conartist Ada-lovelace",
"email": "malicious@example.com"
},
"reference": "Attacker updated the user with ID 23 contact details"
}
当该联系人的所有者登录系统时,他们会发现不同的数据,这是一种风险。他们可能会丢失所有原始数据或数据完整性受到影响。
预防对象级授权失效
由于对象级授权失效会影响企业,因此了解如何防止此类攻击将使企业免于丢失敏感数据。在本节中,我们将讨论处理对象级授权失效的各种方法。
随机生成的唯一标识符
建议使用全局唯一标识符 (GUID) 或通用唯一标识符 (UUID) 来减少可预测对象资源标识符带来的威胁。UUID 更常用,因为它们会生成较大的值(通常是 128 位随机整数),并且发生冲突的可能性较低。当两个或多个标识符指向同一个对象时,就会发生冲突。UUID 字符串的一个示例是092a5461-e71b-32d1-e544-792314582966。
要生成 UUID,请运行以下命令:
pip install uuid
在models.py中,运行以下命令:
import uuid
from django.db import models
class Subscription(models.Model):
id = models.UUIDField(unique=True, primary_key=True, editable=False, default=uuid.uuid4)
...
使用 UUID 使得攻击者更难猜测资源对象标识符。
Django 群组
除了使用 UUID 之外,建立某种访问控制来控制授权和未经身份验证的用户能够操作的数据类型也至关重要。建立访问控制的方法之一是在 Django 中对用户进行分组。这可确保特定用户组可以执行特定操作,从而有助于管理访问。
通过使用 Django Group,我们可以限制对资源对象的访问。例如,在manager.py文件中,运行以下命令:
from django.contrib.auth.models import UserManager, Group
class CustomUserManager(UserManager):
use_in_migration = True
def _create_user(self, email, password, **extra_fields):
"""Create user with a given email and password"""
if not email or not password:
raise ValueError("Users must have an email and password")
user = self.model(email=self.normalize_email(email), **extra_fields)
user.set_password(password)
user.save(using=self._db)
if extra_fields["is_supplier"] == True:
self._add_user_to_group("supplier", user)
...
def _add_user_to_group(self, group_name, user):
user_group, _ = Group.objects.get_or_create(name=group_name)
user_group.user_set.add(user)
...
我们可以创建一个包含 Django 组的自定义用户管理器。根据用户角色(例如供应商),我们可以在上面的代码片段中创建用户时将其分配到组中。在models.py中,运行以下命令:
import uuid
from django.db import models
from django.contrib.auth.models import AbstractUser
from managers.py import CustomUserManager
class User(AbstractUser):
id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
...
objects = CustomUserManager()
然后,我们可以在 User 资源中使用自定义用户管理器。虽然对象级授权没有发生太多变化,但我们已经成功为在平台上注册的所有用户创建了权限组,这是对资源对象进行访问控制的一步。在业务逻辑中,我们可以将内容限制为供应商组中的用户。例如,在views.py中使用 Django REST 框架,运行以下命令:
from rest_framework import viewsets, status, permissions
from rest_framework.response import Response
...
class ReportView(viewset.ModelViewSet):
queryset = Report.objects.all()
permission_classes = [permissions.IsAuthenticated]
def list(self, request):
if request.user.groups.filter(name="supplier").exists():
# PERFORM ACTIONS FOR SUPPLIERS ONLY
return Response({
"detail": "You do not have permission to perform this action"
}, status=status.HTTP_403_FORBIDDEN)
代码片段根据用户组筛选经过身份验证的用户。如果用户不属于供应商组,则他们无法访问资源内容。
我们可以注意到,如果用户属于供应商组,那么无论他们是否是资源对象的所有者,他们都可以访问数据。
资源所有权
为了确保不会将数据泄露给其他用户,我们将有关该资源对象创建者的信息存储在对象中。例如,假设属于供应商组的用户创建了一个报告实例。为了跟踪谁拥有该报告实例,我们在资源数据中添加了一个名为“用户”或“所有者”的字段,如下所示。
在models.py中,运行以下命令:
import uuid
from django.db import models
class Report(models.Model):
id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
owner = models.ForeignKey("User", on_delete=models.CASCADE, related_name=reports)
title = models.CharField(max_length=250)
...
在这里,我们通过将外键分配给用户资源将报告附加到其所有者。
{
"owner": "as34-ade42315d-893a-0934d",
"title": "An unsecure report that exposes the owner ID"
}
现在,前端将提供如上所示的 JSON 对象。
从身份验证令牌中提取 ID
为了防止前端提供报告创建者,我们从经过身份验证的用户那里获取它。我们更新views.py,如下所示。
from rest_framework import viewsets, status, permissions
from rest_framework.response import Response
...
class ReportView(viewset.ModelViewSet):
queryset = Report.objects.all()
permission_classes = [permissions.IsAuthenticated]
serializer_class = ReportSerializer
def create(self, request):
serializer = self.serializer_class(request.data)
if serializer.is_valid(raise_exception=True):
serializer.save(user=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.error_messages, status=status.HTTP_400_BAD_REQUEST)
前端客户端只需要提供报表数据,而不需要明确给出“所有者”字段数据。
{
"title": "This report no longer requires the owner ID on creation"
...
}
按所有权查询
尽管报告现在属于特定用户,但我们仍会检索平台上的所有报告,无论所有权如何。为了防止查看他人的报告,我们可以按所有权过滤查询,如下所示。
from rest_framework import viewsets, status, permissions
from rest_framework.response import Response
...
class ReportView(viewset.ModelViewSet):
queryset = Report.objects.all()
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
if not request.user.is_superuser:
return Report.objects.filter(user=request.user)
return super().get_queryset()
代码片段确保只有经过身份验证的用户的报告才可用。如果用户是超级用户(即管理员),则经过身份验证的用户可以查看所有报告。解决对象级授权的另一种方法是扩展 Django(Django REST 框架)中可用的基本权限,如下所示。
在permissions.py中,运行以下命令:
from rest_framework import permissions
class IsOwnerOrAdmin(permissions.BasePermission):
def has_permission(self, request, view):
# This method only allows suppliers or admin to view this route.
return request.user.groups.filter(name="supplier") or request.user.is_superuser
def has_object_permission(self, request, view, obj):
# This method authorizes only the resource owner or admin from using object.
return obj.owner == request.user or request.user.is_superuser
在上面的代码片段中,has_permission()方法负责授权用户查看或调用特定资源或端点。has_object_permission ()方法负责检查某个用户是否可以访问给定资源中的特定对象。在上面的示例中,只有供应商组中的用户或管理员才能查看资源或路线。为了对资源中的特定对象执行操作,用户必须是对象所有者或平台上的管理员。
要使用此功能,请在views.py中运行以下命令:
from rest_framework import viewsets, status, permissions
from rest_framework.response import Response
from .permissions import IsOwnerOrAdmin
...
class ReportView(viewset.ModelViewSet):
queryset = Report.objects.all()
permission_classes = [permissions.IsAuthenticated, IsOwnerOrAdmin]
...
请注意,现在GET请求中的URL www.example.com/api/reports/仍将向供应商组中经过身份验证的用户显示所有报告。但是,如果经过身份验证的用户不是对象所有者,则禁止对特定资源对象本身执行任何操作。为了确保它仅显示拥有报告的经过身份验证的用户,我们必须将其包含在查询集中,如下所示。
from rest_framework import viewsets, status, permissions
from rest_framework.response import Response
from .permissions import IsOwnerOrAdmin
...
class ReportView(viewset.ModelViewSet):
queryset = Report.objects.all()
permission_classes = [permissions.IsAuthenticated, IsOwnerOrAdmin]
...
def get_queryset(self):
if not request.user.is_superuser:
return Report.objects.filter(user=request.user)
return super().get_queryset()
这些是一些已知的防止 Django API 中的对象级身份验证中断的方法。
信任是靠努力赢得的
随着攻击者不断发现每个系统的安全漏洞,我们必须通过验证数据来源来确保用户数据受到保护。我们永远不应该相信用户提供的数据。为了避免因在企业中存储危险数据而导致灾难,我们必须审查数据。
我们在这篇文章中了解到攻击者如何尝试通过将资源 ID 更改为从可预测的 API ID 结构推断出的新资源 ID 来获取对资源对象的授权。在授权某些资源操作之前未测试经过身份验证的用户权限的端点可能会让攻击者获得对资源的访问权限。
我们考虑了各种方法来对抗这些已知的对象级授权漏洞,即鼓励使用可生成长串低冲突 ID 的 UUID。我们还演示了通过将所有者数据附加到查询期间要访问的资源对象来防止泄露其他用户数据的方法。
文章来源:Django Broken Object-Level Authorization Guide: Examples and Prevention