如何使用Bandit保护Python网络应用程序(附实例)

354 阅读11分钟

如何使用Bandit保护Python Flask应用程序

网络安全是软件开发的一个非常重要的方面。它是代码测试的一个分支,在世界各地的主要科技公司的盈利能力中是不可或缺的,正如我之前关于静态代码分析的文章中所强调的。几周前,当我在寻找测试我的flask项目可能存在的安全漏洞的方法时,我偶然发现了一个叫Bandit的工具。Bandit是一个为定位和纠正Python代码中的安全问题而开发的工具。为了做到这一点,Bandit分析了每个文件,从中建立了一个AST,并对AST节点运行合适的插件。一旦Bandit完成了对所有文件的扫描,它就会生成一份报告。

Bandit在检测安全问题方面非常有用,甚至在kali的博客上被誉为寻找项目中常见安全问题的最佳工具之一。现在我们知道了Bandit是什么,让我们看看测试我们的代码是否存在安全漏洞的一些优势。

确保我们的代码安全的重要性

  • 不安全的代码很容易受到外部威胁,个人信息或公司机密被泄露,如果被利用的话,可能会导致大量的金钱损失。因此,确保我们的代码安全对于避免这一问题非常重要。
  • 不安全的代码还可能导致对利用该软件的成千上万的用户的系统造成损害。这也可能使公司在赔偿受影响的用户方面花费大量的金钱。
    确保我们的代码安全也将解决这个问题。
  • 不安全的代码会导致生命和财产的损失。一些恶意的组织利用软件,窃取用户的数据来勒索他们。这可能会导致用户自杀或交易他们的财产来释放自己。像这样的事件可以通过简单地制作安全代码来避免。
  • 我们现在知道了为什么我们应该保护我们的代码。现在我们可以通过使用Bandit工具来弄脏我们的手。

Bandit工具

Bandit可以通过命令行安装在你的系统中:

pip install bandit

编写和分析我们的代码

现在,我们将编写python代码,用flask收集用户的姓名和电子邮件,并使用python flask框架将其存储在数据库中,然后使用bandit工具分析代码中的安全漏洞问题。

先决条件

Python、Flask、SQLAlchemy和Bandit。

用我们的命令外壳安装我们的依赖项

$ pip install bandit
$ pip install flask
$ pip install flask-sqlalchemy

# to check if bandit installed successfully
$ bandit --version
$ bandit 1.7.0

现在我们已经成功地安装了我们的依赖项,我们可以编写我们要测试的flask APP。

我们的Flask应用

让我们开始在我们的程序中导入所需的模块或库:

#Importing Libraries
from flask import Flask,jsonify
from flask_restful import  Api, Resource
from flask_sqlalchemy import SQLAlchemy

接下来,我们初始化我们的flask,并使用我们的app config和SQLAlchemy配置我们的数据库。我们的数据库文件名将是data.sqlite3:

app = Flask(__name__)
application=app
api = Api(app)
db = SQLAlchemy(app)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.sqlite3'

现在我们将使用SQLAlchemy完成的ORM(对象关系映射)来创建我们的数据库表和它们的结构,而我们的表名就是类名,类属性就是表的字段。然后我们使用命令create_all来创建我们的数据库文件:

class User(db.Model):
   id = db.Column('id', db.Integer,primary_key=True)
   name = db.Column('name', db.String(100), nullable=True)
   email = db.Column('email', db.String(100), nullable=True)
   phone = db.Column('phone', db.String(100), nullable=True)

def __init__(self,id,name,email):
   self.id = id
   self.name = name
   self.email = email
   self.phone = phone

db.create_all()

现在我们将定义我们的API端点 "save",它将把用户的姓名和电子邮件保存到数据库中。在保存之前,我们只需检查用户提供的电子邮件是否已经存在于数据库中。为了保存,我们定义一个数据库类 "User "的对象,并将这个对象实例添加到数据库中:

class save(Resource):
    def get(self):
        name=request.form.get("name")
        if name:
            pass
        else:
            name= request.args.get("name")
        email=request.form.get("email")
        if email:
            pass
        else:
            email=request.args.get("email")
        phone=request.form.get("phone")
        if phone:
            pass
        else:
            phone=request.args.get("phone")
        if User.query.filter_by(email=email).first() or User.query.filter_by(phone=phone).first():
            return jsonify({"success": False, "message": "Email already saved"})
        user = User(name=name,email=email)
        db.session.add(user)
        db.session.commit()

        return jsonify({"success": True, "message": "data saved"})

最后,我们可以为上面刚创建的保存资源添加一个路由:

#adding resouce with route
api.add_resource(save, '/save')

if __name__ == '__main__':
    app.run(debug=True)

如果你成功地走到这个阶段而没有出错,恭喜你,我们现在可以用Bandit测试安全漏洞了。

在没有配置文件或基线的情况下使用Bandit

我们将尝试使用我们的探矿器工具,而不对基线或配置文件进行任何定制配置:

$ bandit collect.py

下面是在我们的flask应用上运行Bandit的结果

 Bandit result on Flaskapp

Flask应用上的Bandit结果。

在上面的图片中,我们可以看到bandit突出了一个错误信息 "B201:flask_debug_true"。Bandit还解释了这个错误发生的原因。这个错误信息是由一个漏洞问题引起的,这个漏洞问题是在程序中调试设置为真时发生的。当你的代码中存在这个安全问题时,黑客可以模拟错误,导致调试页面的显示,系统密匙、环境变量和其他重要信息将被暴露。这将允许黑客缩小他们的攻击视角,用各种技术进行攻击,最有可能的是使用Django应用程序的秘密密钥来捕获和解密请求,或者在我们的案例中进行Dos攻击。

现在让我们在另一段代码上测试bandit,这样我们可以看到bandit可以指出的其他漏洞。下面是一个Python Django代码中的视图,它接收学生的二维码图像并解码,然后用Vonage向家长的电话号码发送短信。

def decode_qr(request):
    if request.method == "POST":
        image = request.FILES['snapshot']
        m = qr_test.objects.create(image=image)
        m.save()
        link = "https://school.advancescholar.com/media/" + str(image).replace(" ","")
        r = requests.get(
            "http://api.qrserver.com/v1/read-qr-code/", params={"fileurl": link})
        try:
            qr = r.json()[0]["symbol"][0]
            if qr["error"]:
                data = None
            else:
                data = qr["data"]
        except:
            data = None
        valid = UserProfile.objects.all().filter(name=data, user_type="Student")
        print(data)
        if valid:
            client = nexmo.Client(key='key', secret='secret_key')
            text='Your child '  + data + " arrived school at " + str(datetime.datetime.now())
            client.send_message({
                'from': 'Vonage APIs',
                'to': 'parent_number',
                'text': text,
            })
            return render(request, "qr_code.html", {"message": data + " is a registered student and attendance marked"})
        else:
            return render(request,"qr_code.html", {"message":" Not a registered student"})
    return render(request, "qr_code.html")

下面是在这段代码上运行bandit的结果:

bandit result on Django code

bandit对Django代码的结果在上面的图片中,我们可以看到bandit突出了一个错误信息 "B106:hardcoded_password_funcarg"。Bandit还解释了这个错误发生的原因。这个错误信息是我们在程序中硬编码Venmo密匙的结果。通常情况下,这个密钥可以存储在一个单独的文件中,不会被推送到GitHub。这个漏洞可以让黑客窃取秘钥并获得对你的Venmo应用程序的访问权。这使他们能够利用你的Venmo账户,并获得该应用程序的使用细节。

Bandit基线和配置文件定制

基线定制

Bandit允许指定基线记录的方向,根据底线参数的使用情况进行评估(即-b BASELINE--baseline BASELINE )。这有利于忽略您认为的非问题的漏洞(例如,单元测试中的明文密码)。要生成基线记录,请明确运行Bandit,输出布局设置为JSON(最有效的JSON格式的文件作为基线是很常见的),并指定输出记录课程。

$ bandit -f json -o PATH_TO_OUTPUT_FILE

配置文件定制

Bandit可以使用配置文件运行。在运行Bandit时,只需使用ShellInjection配置文件中索引的插件。

$ bandit examples/*.py -p ShellInjection

漏洞测试

漏洞测试是一种软件测试技术,用于评估系统中涉及的风险大小,以减少有害事件发生的概率。

Bandit支持编写不同的测试来检测您的python代码中的各种安全问题。

漏洞测试是用Python编写的,并从插件目录中自动发现。每个测试可以评估您的Python语句的一种或多种类型。测试以其评估的Python语句类型为标签(例如:函数调用,字符串,导入,等等)。

测试由BanditNodeVisitor对象执行,它检查AST中的每个节点。测试结果在管理器中进行管理,并在测试运行完成后通过管理器实例的output_result方法进行汇总输出。

然而,Bandit 包含了一个漏洞检查的列表,可以从中选择。要为你需要的漏洞配置bandit,你必须编辑YAML配置文件。

为漏洞测试进行配置

Bandit 被定义为可配置的,并涵盖大量的安全需求。这就是bandit的特别之处,它既可以作为本地开发者的工具,也可以作为完整的CI/CD管道的一部分。 就像我上面说的,bandit 漏洞测试可以通过 YAML 配置文件进行配置。

下面是所有 bandit 漏洞测试插件的列表

  • B604: any_other_function_with_shell_equals_true
  • B101: assert_used
  • B102: exec_used
  • B111: execute_with_run_as_root_equals_true
  • B201: flask_debug_true
  • B104: hardcoded_bind_all_interfaces
  • B106: hardcoded_password_funcarg
  • B107: hardcoded_password_default
  • B105: Hardcoded_password_string
  • B608:hardcoded_sql_expressions
  • B108: Hardcoded_tmp_directory
  • B701: jinja2_autoescape_false
  • B609: linux_commands_wildcard_injection
  • B601:paramiko_calls
  • B109: password_config_option_not_marked_secret
  • B501: request_with_no_cert_validation
  • B103: set_bad_file_permissions
  • B503: ssl_with_bad_defaults
  • B502: ssl_with_bad_version
  • B504: ssl_with_no_version
  • B605: 用外壳启动进程
  • B606: 不带壳启动进程
  • B607: 用部分路径启动进程
  • B602: subprocess_popen_with_shell_equals_true
  • B603: Subprocess_without_shell_equals_true
  • B112: try_except_continue
  • B110: try_except_pass
  • B702: use_of_mako_templates
  • B505: weak_cryptographic_key
  • B506:yaml_load

在我们的 bandit 配置文件中,我们可以决定选择我们想要运行的特定测试插件,并覆盖测试的原始配置。在我们的配置文件中的一个例子如下所示:

### profile may optionally select or skip tests

# (optional) list included tests here:
tests: ['B201', 'B301']

# (optional) list skipped tests here:
skips: ['B101', 'B601']

### override settings - used to set settings for plugins to non-default values

any_other_function_with_shell_equals_true:
  no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve,
    os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe,
    os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, os.startfile]
  shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4,
    popen2.popen2, popen2.popen3, popen2.popen4, popen2.Popen3,
    popen2.Popen4, commands.getoutput,  commands.getstatusoutput]
  subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call,
    subprocess.check_output]

在上面的配置文件中,我们做的第一件事是定义我们在用Bandit测试时要采用的测试。配置 "测试。['B201', 'B301']"设置bandit只测试我们这两个插件的代码。 下一件事是定义我们要跳过的测试插件,因为我们没有利用它们。然而,如果你只想控制要运行的特定测试(而不是它们的参数),那么在命令行上使用 -s 或 -t 可能更合适。

我们所做的最后一件事是将各种测试插件上的默认设置覆盖为我们所熟悉的或需要测试我们代码的值。

注意:如果你只想定义要运行的特定测试(而不是它们的参数),在命令行上使用-s或-t。

在我们需要为特定功能运行几个测试的情况下,我们将创建几个配置文件,并使用-c下标从中挑选。

只处理一行代码

如果你的代码中有一些行导致了错误,而且你确信这是可以接受的,可以通过在该行中添加# nosec来单独消音,如下图所示。

# The following hash is not used in any security context. It is only used
# to generate unique values, collisions are acceptable and "data" is not
# coming from user-generated input
the_hash = md5(data).hexdigest()  # nosec

自动生成我们的配置文件

Bandit-config-generator旨在消除配置方面的工作负担。它生成一个自动配置。生成的配置将包括所有检测的测试和黑名单插件的默认配置块。然后可以删除或编辑这些数据,以便根据需要产生一个最小的配置。配置生成器支持-t和-s命令行选项,分别指定应包括或排除的测试ID列表。如果没有给出其他选项,那么生成的配置文件将不包括任何测试或跳过部分(然而,它将提供一个所有测试ID的诅咒列表,以便编辑时参考)。

配置我们的测试插件

Bandit配置文件以YAML格式编写。每个测试插件的选项都在一个与测试方法相匹配的部分下提供。

例如,给定一个名为 "check_if_good "的测试插件,其配置部分可能看起来像下面这样:

check_If_good:
  check_typed_exception: True

编写我们的Bandit测试

根据bandit 官方文档,编写 bandit 漏洞测试时,我们需要遵循以下步骤:

  • 确定一个需要测试的漏洞,并在 examples/中创建一个新文件,其中包含该漏洞的一个或多个案例。
  • 创建一个新的Python源文件来包含你的测试,你可以参考现有的测试作为例子。
  • 考虑你要测试的漏洞,用一个或多个适当的装饰器标记该函数:
    @checks('Call')
    @checks('Import', 'ImportFrom')
    @checks('Str')
  • 使用 bandit.plugins 入口点注册你的插件,见一个例子。
  • 你创建的函数应该接受一个参数 "context",这是一个上下文类的实例,你可以查询当前被检查元素的信息。你也可以为更高级的使用情况获得原始AST节点。请参阅context.py文件以了解更多。
  • 根据需要扩展Bandit配置文件以支持你的新测试。
  • 针对你在examples/中定义的测试文件执行Bandit,并确保它能检测到该漏洞。考虑该漏洞可能出现的变化,并相应地扩展示例文件和测试功能。

当试图编写一个新的测试插件来测试你的代码时,这些步骤非常有帮助。

让我们看看一个按照上述步骤编写的漏洞测试的例子。

@bandit.checks('Call')
def prohibit_unsafe_deserialization(context):
    if 'unsafe_load' in context.call_function_name_qual:
        return bandit.Issue(
            severity=bandit.HIGH,
            confidence=bandit.HIGH,
            text="Unsafe deserialization detected."
        )

在上面的例子中,首先在examples/中创建了一个新文件,然后定义了一个要对代码进行测试的名为 "prohibit_unsafe_deserialization "的漏洞。这个函数检查你的代码中是否有不安全的上下文被传递。我们已经完成了官方文档中的第一和第二步骤。编写完我们的函数后,下一步是添加上面第三步中所要求的bandit装饰器。对于这个特殊的测试,使用了"@call "装饰器,因为上下文被函数调用了。添加装饰器后,下一步是使用 bandit.plugins 入口点注册你的插件,如上所述。为了注册上面的测试插件,有两种方法可以采用。

  • 如果你想直接使用 setuptools,你需要在你的 setup 调用中加入类似以下内容:
# If you have an imaginary bson formatter in the bandit_bson module
# and a function called `formatter`.
entry_points={'bandit.formatters': ['bson = bandit_bson:formatter']}
# Or a check for using mako templates in bandit_mako that
entry_points={'bandit.plugins': ['mako = bandit_mako']}
  • 如果你使用pbr,更简单的方法是在你的setup.cfg文件中加入类似下面的内容:
[entry_points]
bandit.formatters =
    bson = bandit_bson:formatter
bandit.plugins =
    mako = bandit_mako

现在你可以根据需要扩展你的bandit配置文件,以支持测试,最后针对你的代码运行bandit,检测由你编写和定制的安全漏洞。

一些常见的插件标识分组

下面是bandit中一些常见的漏洞测试插件和它们的描述:

B1xx -misc测试
B2xx-应用/框架错误配置
B3xx-黑名单(调用)
B4xx-黑名单(导入)
B5xx-加密技术
B6xx-注入
B7xx-XSS

总结

  • 我们了解了网络安全以及为什么它在软件开发中很重要。
  • 我们了解了Bandit,以及为什么它在检测简单的漏洞问题方面很有用。
  • 我们学习了如何使用和定制Bandit来检测我们代码中的简单安全漏洞。
  • 我们学习了如何编写和配置我们自己的Bandit测试插件,并将它们扩展到我们的配置文件中,以测试我们代码中的漏洞。