学习Python中断言的危险性

133 阅读9分钟

有很多方法可以发现Python代码中的bug:内置的调试器(pdb)、健康数量的单元测试、像Pycharm或Visual Studio这样的IDE中的调试器、try/catch 语句、if/else 语句、assert statements ,或者用print() 语句覆盖你的代码库的每一寸地方,就像它要过时一样的尝试和真实做法。

断言语句可以帮助我们快速捕捉错误,而且比大量的print 语句要少得多。然而,与print 语句不同,断言语句的使用可能会有意想不到的风险!

这篇文章探讨了如何安全地使用断言,以及什么原因导致它们不安全。在本文结束时,你将知道如何最优化地使用assert ,而不会在无意中使自己陷入安全问题。

使用 Python 断言语句

使用 assert 很简单!断言语句是简单的语句,这意味着它们可以放在一行 Python 代码中。

下面是一个assert 语句的简单例子。

assert expression

在上面的语句中,你在断言表达式的值是True ,类似于布尔检查。

如果表达式是False ,就会抛出一个异常。比如说。

>>> assert 'hi' == 'there'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

为了更容易筛选堆栈跟踪,你可以指定一个断言信息,它将作为AssertionError 的一部分。这在复杂的调试场景中可能很有用。

>>> assert 'hi' == 'there', 'Values do not match!'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: Values do not match!

当使用断言语句时,Python 内置的__debug__ 变量必须被设置为True 。如果它被设置为False ,断言语句在运行时就不会执行。这是一个严重的问题,因为在优化模式下运行 Python (在生产环境中通常通过设置PYTHONOPTIMIZE命令行标志-O)会将__debug__ 变量设置为False ,从而禁用断言语句,这可能会在无意中给你的项目引入漏洞。我们将在下面更详细地探讨这个问题。

如果使用得当,assert 是一个强有力的调试工具。然而,用法越复杂,在开发过程中引入安全漏洞的空间就越大。

TL;DR - 断言语句对于调试Python应用程序是非常有用的,但不能用于正常的错误处理或控制流,因为它们要么。

  • 通过引发异常来停止应用程序,这可能导致生产中断。
  • 当调试模式被禁用和断言Python运行时默默地跳过语句时,在你的应用程序中引起意外的副作用(可能包括漏洞)。

安全地使用 assert

让我们通过研究一个计算简单利息的示例程序,来探索如何使用断言语句来调试程序。

首先,安装一个稳定的 Python 版本。接下来,打开一个文件夹,创建一个名为safe_assert_example.py 的 Python 文件。你可以使用任何支持的代码编辑器。

safe_assert_example.py 中加入以下代码。

def simpleInterest(p, t, r):
print('The principal is', p)
assert isinstance(p, int) and p > 0, "Principal must be a positive integer"
print('The time period is', t)
assert isinstance(t, int) and t > 0, "Time must be a positive integer"
print('The rate of interest is', r)
assert isinstance(r, int) and r > 0, "Rate of interest must be a positive integer"
simpleInterest = (p * t * r) / 100
print('The Simple Interest is', simpleInterest)
return simpleInterest

simpleInterest(3, 5, 8)

这个程序定义了simpleInterest 函数,用本金、时间和利率计算单利。

为了正确计算利息,你必须为本金、时间和利率输入正整数。本例使用断言语句来检查p,t, 和r 的值是否为正整数。当一个值不是正整数时,程序会返回一个信息性的错误信息。

是时候试试了!使用以下命令运行该程序。

python safe_assert_example.py

你应该得到下图所示的输出,单利为1.2。

PS E:\Sample\Extra\Assert Demo> python safe_assert_example.py
The principal is 3
The time period is 5
The rate of interest is 8
The Simple Interest is 1.2

正如你所看到的,该程序运行平稳,没有返回错误信息。

现在,看看当你试图用无效的输入运行这个程序时会发生什么。试着把t 的值从5 改为字符串 "time"。

simpleInterest(3, "time", 8)

现在,用这个命令重新运行这个程序。

python safe_assert_example.py

这一次,你应该看到以下错误。

PS E: \Sample\Extra\Assert Demo» python safe assert example.py
The principal is 3
The time period is time
Traceback (most recent call last):
    File "E: \Sample\Extra\Assert Demo\safe assert example.py", line 12, in <module> simpleInterest(3, "time", 8)
    File "E: \Sample\Extra\Assert Demo\safe assert example.py", line 5, in simpleInterest assert isinstance(t, int) and t > 0, "Time must be a positive integer"
AssertionError: Time must be a positive integer

这个输出包括一个AssertionError 。这个异常强调了断言语句按预期工作,因为它成功地捕捉到了无效的函数参数。

在这个例子中,断言帮助我们调试程序*,*一旦断言失败,就停止程序的执行。

回顾一下,在优化模式下运行程序(通过设置PYTHONOPTIMIZE 命令行标志,-O )会默默地禁用断言语句。现在,看看这种行为是如何影响应用程序的。

保持"`时间="作为t ,用以下命令在优化模式下执行简单兴趣程序。

python -O safe_assert_example.py

这时返回。

PS E:\Sample\Extra\Assert Demo› python -0 safe assert example.py
The principal is 3
The time period is time
The rate of interest is 8
Traceback (most recent call last):
    File "E:\Sample\Extra\Assert Demo\safe assert example.py", line 12, in <module> simpleInterest(3, "time", 8)
    File "E:\Sample\Extra\Assert Demo\safe assert example.py", line 8, in simpleInterest simpleInterest = (p * t * r)/100
TypeError: unsupported operand type(s) for /: 'str' and 'int'

注意,即使程序有一个无效的函数参数,上面的输出也不包括预期的AssertionError 。相反,Python 输出一个TypeError ,因为你试图将整数和字符串类型的变量相乘。这种情况的发生是因为断言语句没有运行,并建议我们应该考虑其他方法,比如。

  • 在代码运行前添加类型提示以帮助捕捉对simpleInterest 的不正确调用。
  • 在运行时使用if 语句而不是断言来检查类型。

不安全地使用断言

断言应该只用于测试和调试 - 而不是在生产环境中。因为 assert 语句只在__debug__ 变量被设置为True 时运行,所以 Python 解释器可以禁用它们。

正如上面的例子所展示的,在优化模式下运行 Python,使用-O 可以阻止 assert 语句的运行,因为它将__debug__ 设置为False

正因为如此,在需要在生产环境中运行的情况下使用断言语句是不安全的。例如,你不应该使用断言来验证令牌或用户输入。这些语句不会在生产环境中运行,所以任何令牌或用户输入都不会被验证,可能会在你的应用程序中引入漏洞。

为了强调在生产环境中使用assert进行验证的严重性,考虑一个用户授权函数,该函数接收一个用户数组,然后允许他们根据访问级别进一步移动。该程序首先使用assert来验证列表或数组是否为空或不属于使用assert的列表实例。然后,根据角色,它将允许管理员完全访问该应用程序。如果在用户列表中发现了一个管理员角色,那么就不会提示断言错误。但是,如果我们在列表中没有看到管理员角色,它就会出现错误。

接下来,将这段代码粘贴到一个名为unsafe_assert_example.py 的文件中。

def authorize_admin_user(user_roles):
    assert isinstance(user_roles,list) and user_roles != [], "No user roles found"

    assert 'admin' in user_roles, "No admin role found."
    print("You have full access to the application.")

authorize_admin_user(['admin','user'])

该函数authorize_admin_user ,检查用户的角色。如果他们是管理员,它将显示我们有对应用程序的完全权限。通常情况下,用户的角色是从数据库中加载的;如果管理员是其中之一,则用户被授予对应用程序的完全访问权。否则,断言将失败并抛出一个异常。

文件的最后一行调用authorize_admin_user 函数,传入一个包含两个角色的列表:admin和user。

Run python unsafe_assert_example.py:

$ python unsafe_assert_example.py
You have full access to the application

正如预期的那样,用户被授予对应用程序的完全访问权。

让我们在列表中不包含管理员角色的情况下调用用户角色函数。将unsafe_assert_example.py 中的最后一行代码改为。

authorize_admin_user(['user'])

然后,运行该程序并观察输出。

$ python unsafe_assert_example.py
Traceback (most recent call last):
  File "C:\Projects\ContentLabEditing\python\unsafe_assert_example.py", line 6, in <module>
    authorize_admin_user(['user'])
  File "C:\Projects\ContentLabEditing\python\unsafe_assert_example.py", line 3, in authorize_admin_user
    assert 'admin' in user_roles, "No admin role found."
AssertionError: No admin role found.

正如所料,该函数抛出了一个AssertionError ,因为管理员不在角色列表中。

现在,运行同样的程序,优化输出。

python -O unsafe_assert_example.py

输出将与下面的图片一致。尽管列表中没有admin 角色,但由于断言没有运行,所以管理员授权成功了。

PS E:\Sample\Extra\Assert Demo> python -0 unsafe_assert_example.py
You have full access to the application
You have full access to the application
PS E:\Sample\Extra\Assert Demo>

此外,在相同的优化输出执行中,用整数列表改变函数调用,仍然会展示assert应该只用于调试目的,而不是验证。

将函数调用改为authorize_admin_user([1,2]) ,并使用相同的命令python -O unsafe_assert_example.py ,运行它。

程序的输出仍然是一样的。

在我们简单的利息计算器的例子中,当我们的断言语句没有运行时,后果是轻微的。虽然我们没有收到我们想要的错误信息,但我们可以使用Python的错误信息来了解出错的原因。更重要的是,我们没有引入任何漏洞,因为我们是为了安全调试而使用 assert 语句。

在这个例子中,我们不安全地使用了 assert,并通过授予一个非管理员用户以管理员权限而使我们的 Python 应用程序变得脆弱。

用断言做什么和不做什么

Assert 可以帮助开发者轻松调试 Python 代码,同时减少代码行数。然而,如果不安全地使用,它可能会在无意中给你的应用程序带来安全问题。

在生产环境中禁用断言可能是毁灭性的。这种做法会在应用程序中引入各种后门和断点,让坏人有机可乘。

断言的主要功能应该是隔离错误的根源,而不应该被用于标准的错误处理。虽然断言可能是有益的,但必须将其用于调试和测试,而不是控制程序或应用程序的逻辑流程,并且只在安全情况下使用。如果你在寻找assert的替代品,你会发现使用单元测试、调试器、条件式和验证器是更好、更安全的选择。