Yelp/fuzz-lightyear

GitHub: Yelp/fuzz-lightyear

一个基于 pytest 风格的 DAST 框架,通过有状态的 Swagger 模糊测试在微服务生态中发现安全漏洞。

Stars: 226 | Forks: 27

[![构建状态](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/351b1a0690071909.svg)](https://github.com/Yelp/fuzz-lightyear/actions/workflows/fuzz-lightyear-build.yml) # fuzz-lightyear

fuzz-lightyear 是一个[受 pytest 启发](https://docs.pytest.org/en/latest/)的, [DAST](https://en.wikipedia.org/wiki/Dynamic_Application_Security_Testing) 框架, 能够通过 **有状态的** [Swagger](https://swagger.io/) 模糊测试,识别分布式微服务生态系统中的漏洞。 **有状态模糊测试的特殊之处?** 传统的模糊测试基于这样一个假设:命令调用失败 意味着存在漏洞。这种方法并不适用于 Web 服务模糊测试, 因为在错误输入时发生失败是**预期之中的**——事实上,包含故意构造的 恶意 payload 的成功请求反而更加危险,理应被 捕获。 有状态的模糊测试使我们能够做到这一点。通过在请求之间保持状态,我们可以 组装一个请求序列,并对其进行精心设计以模拟恶意的攻击向量,然后针对 意外的成功发出警报。使用[假设检验](https://hypothesis.readthedocs.io/en/latest/), 我们能够动态生成这些测试用例,以便我们能够持续发现新的 攻击向量。最后,当我们发现错误时,这个测试框架会输出一系列 cURL 命令,方便轻松复现。 ## 示例 ``` $ fuzz-lightyear https://petstore.swagger.io/v2/swagger.json -v --ignore-exceptions ``` ## 安装 ``` pip install fuzz-lightyear ``` ## 使用方法 ``` $ fuzz-lightyear -h usage: fuzz-lightyear [-h] [-v] [--version] [-n [ITERATIONS]] [--schema SCHEMA] [-f FIXTURE] [--seed SEED] [-t TEST] [--ignore-exceptions] [--disable-unicode] url positional arguments: url URL of server to fuzz. optional arguments: -h, --help show this help message and exit -v, --verbose Increase the verbosity of logging. --version Displays version information. -n [ITERATIONS], --iterations [ITERATIONS] Maximum request sequence length to fuzz. --schema SCHEMA Path to local swagger schema. If provided, this overrides theswagger file found at the URL. -f FIXTURE, --fixture FIXTURE Path to custom specified fixtures. --seed SEED Specify seed for generation of random output. -t TEST, --test TEST Specifies a single test to run. --ignore-exceptions Ignores all exceptions raised during fuzzing (aka. only fails when vulnerabilities are found). --disable-unicode Disable unicode characters in fuzzing, only use ASCII. --depth DEPTH Maximum depth for generating nested fuzz parameters. ``` ## Fixtures Fixtures 是 `fuzz-lightyear` 的核心组件,允许你自定义工厂, 以补充各种 endpoint 的模糊测试工作。这对微服务生态系统来说 至关重要,因为服务本身可能并不是 CRUD 应用程序。 这意味着 Swagger 规范中可能不包含作为请求序列一部分用于创建 临时资源的 endpoint。 为了解决这个问题,我们允许开发者提供必要的自定义命令,以填充 被模糊测试的请求参数的某些部分。 ### 示例 假设我们有以下 Swagger 代码片段: ``` paths: /biz_user/{userID}/invoices: get: tags: - business operationId: "get_business_by_id" parameters: - name: "userID" in: "path" required: true type: integer responses: 200: description: "success" 403: description: "forbidden" 404: description: "business not found" ``` 我们需要一个有效的 `userID` 才能访问其 invoices。显然,对模糊测试器来说, 为 `userID` 填入随机值是浪费时间,因为我们并不关心攻击者是否 尝试访问一个不存在的业务。此外,该服务 并不了解如何创建业务(以获取有效的 `userID`),因此模糊测试器 在测试此 endpoint 时将毫无效果。 为了解决这个问题,我们定义了一个 fixture,来告诉 `fuzz-lightyear` 如何处理此类 情况。 ``` # fixtures.py import fuzz_lightyear @fuzz_lightyear.register_factory('userID') def create_biz_user_id(): return 1 ``` 现在,当 `fuzz-lightyear` 尝试对 `/biz_user/{userID}/invoices` 进行模糊测试时,它将 识别出 `userID` 存在一个用户定义的工厂,并在模糊测试中 使用其值。 ``` $ fuzz-lightyear -f fixtures.py http://localhost:5000/schema -v ================================== fuzzing session starts ================================== Hypothesis Seed: 152367346948224061420843471695694220247 business E ====================================== Test Failures ======================================= _________________________ business.get_business_by_id [IDORPlugin] _________________________ Request Sequence: [ "curl -X GET http://localhost:5000/biz_user/1/invoices" ] ================================== 1 failed in 1.2 seconds ================================= ``` 我们可以通过在 `create_business` 函数中指定一个自定义方法来创建业务, 从而修改此示例。 ### 嵌套 Fixtures 继续上面的示例,假设你需要先拥有一个业务,然后 才能创建一个 biz_user。我们可以通过以下方法来实现: ``` # fixtures.py import fuzz_lightyear @fuzz_lightyear.register_factory('userID') def create_biz_user_id(businessID): return businessID + 1 @fuzz_lightyear.register_factory('businessID') def create_business(): return 1 ``` 然后, ``` $ fuzz-lightyear -f fixtures.py http://localhost:5000/schema -v ================================== fuzzing session starts ================================== Hypothesis Seed: 152367346948224061420843471695694220247 business E ====================================== Test Failures ======================================= _________________________ business.get_business_by_id [IDORPlugin] _________________________ Request Sequence: [ "curl -X GET http://localhost:5000/biz_user/2/invoices" ] ================================== 1 failed in 1.2 seconds ================================= ``` 我们还可以通过使用**类型注解**,对嵌套的 fixtures 进行类型转换。 ``` # fixtures.py import fuzz_lightyear @fuzz_lightyear.register_factory('userID') def create_biz_user_id(businessID: str): return businessID + 'a' @fuzz_lightyear.register_factory('businessID') def create_business(): return 1 ``` 这将产生: ``` $ fuzz-lightyear -f fixtures.py http://localhost:5000/schema -v ================================== fuzzing session starts ================================== Hypothesis Seed: 152367346948224061420843471695694220247 business E ====================================== Test Failures ======================================= _________________________ business.get_business_by_id [IDORPlugin] _________________________ Request Sequence: [ "curl -X GET http://localhost:5000/biz_user/1a/invoices" ] ================================== 1 failed in 1.2 seconds ================================= ``` ### 特定 Endpoint 的 Fixtures 假设我们有另一个 endpoint `get_user_by_id`,它需要另一种类型的 `userID`。我们不能使用现有的 `userID` fixture,因为它生成了错误类型的 ID。我们可以通过编写一个特定 endpoint 的 fixture 来解决这个问题。 ``` # fixtures.py import fuzz_lightyear @fuzz_lightyear.register_factory('userID') def create_biz_user_id(businessID): return businessID + 1 @fuzz_lightyear.register_factory('userID', endpoint_ids=['get_user_by_id']) def create_user_id(): return 'foo' @fuzz_lightyear.register_factory('businessID') def create_business(): return 1 ``` 这将产生: ``` ... _________________________ user.get_user_by_id [IDORPlugin] _________________________ Request Sequence: [ "curl -X GET http://localhost:5000/user/foo" ] ================================== 1 failed in 1.2 seconds ================================= ``` 我们也可以将其与嵌套 fixtures 结合使用,但如果我们在 fixture 中指定了一个 endpoint, 那么依赖于该 fixture 的每个 fixture 也都需要指定该 endpoint。 ``` # fixtures.py import fuzz_lightyear # 我们必须在这里指定 get_business_by_id! @fuzz_lightyear.register_factory('userID', endpoint_ids=['get_business_by_id']) def create_biz_user_id(businessID): return businessID + 1 @fuzz_lightyear.register_factory('businessID', endpoint_ids=['get_business_by_id']) def create_business(): return 1 ``` 这将产生: ``` ... _________________________ user.get_user_by_id [IDORPlugin] _________________________ Request Sequence: [ "curl -X GET http://localhost:5000/biz_user/2/invoices" ] ================================== 1 failed in 1.2 seconds ================================= ``` ### 身份验证 Fixtures 我们可以使用 fixtures 来指定 Swagger 规范中的身份验证/授权方法。 这允许开发者根据各自的用例,自定义会话 cookies 或 API tokens 的使用。 这些 fixtures 是 `IDORPlugin` 所必需的。我们可以在 fixture 中包含一个 `operation_id` 参数, 以便自动传入操作 ID。其他参数将不会被模糊测试。 ``` """ These values are passed into the configured request method as keyword arguments. Check out https://bravado.readthedocs.io/en/stable/advanced.html#adding-request-headers for more info. """ import fuzz_lightyear @fuzz_lightyear.victim_account def victim_factory(): return { '_request_options': { 'headers': { 'session': 'victim_session_id', }, } } @fuzz_lightyear.attacker_account def attacker_factory(): return { '_request_options': { 'headers': { 'session': 'attacker_session_id', } } } ``` ### 设置 Fixtures 我们可以使用设置 fixtures 来指定我们希望在运行任何测试_之前_运行的 代码。这允许开发者设置测试应用程序所依赖的 任何自定义配置或外部应用程序。 ``` import fuzz_lightyear @fuzz_lightyear.setup def setup_function(): print("This code will be executed before any tests are run") ``` ### 包含和排除 Swagger 标签与操作 我们可以使用 fixtures 来控制 fuzz-lightyear 是否对 Swagger 规范的某些部分 进行模糊测试。这允许开发者仅对测试环境中可以被模糊测试的 规范部分进行模糊测试。 ``` import fuzz_lightyear @fuzz_lightyear.include.tags def get_tags_to_fuzz(): """fuzz_lightyear will only fuzz operations from these tags. """ return ['user', 'transactions'] @fuzz_lightyear.exclude.operations def get_operations_to_exclude(): """fuzz_lightyear will not call these Swagger operations. """ return [ 'get_user_id', 'operation_doesnt_work_in_test_environment', ] @fuzz_lightyear.exclude.non_vulnerable_operations def get_non_vulnerable_operations(): """fuzz_lightyear will not check these Swagger operations for vulnerabilities. This is different from `fuzz_lightyear.exclude.operations` in that these operations can still be executed by the fuzzer to generate request sequences, but the vulnerability plugins will not verify that these operations are secure. """ # Accessing a user's public profile shouldn't require # authentication. return ['get_user_public_profile'] ``` ### 模糊测试后钩子 有时,工厂 fixtures 和随机模糊测试并不足以 构建一个有效的请求。例如,API 可能有一个未声明的 必需 header,并且将其添加到 Swagger 规范中是不可行的。在这种情况下,我们可以使用模糊测试后的钩子将 模糊测试后的数据转换为有效的形式。 ``` @fuzz_lightyear.hooks.post_fuzz( tags='user', operations='some_function', rerun=True, ) def apply_nonce( operation: bravado.client.CallableOperation, fuzzed_data: Dict[str, Any], ) -> None: """This hook creates and adds a nonce to any request against operations with the 'user' tag, and additionally to the 'some_function' operation. In addition, this nonce cannot be reused by a fuzz-lightyear request object, so we mark this hook is needing to be `rerun`. """ nonce = make_nonce() fuzzed_data['nonce'] = nonce ``` 注意:这些钩子的运行顺序是不保证的。
标签:API安全, CISA项目, cURL, DAST, Fuzzing, JSON输出, pytest, Python, REST API, Swagger, 假设测试, 动态应用安全测试, 安全规则引擎, 开源框架, 微服务安全, 恶意软件分析, 持续集成, 无后门, 模糊测试框架, 混沌工程, 状态模糊测试, 网络安全, 逆向工具, 隐私保护