In this section we'll create a custom bubble and get it loaded into Fizzgun. We are going to start with simple functionality and add more features to it step by step.

Our bubble will try to find authorization issues in our web applications. It will:

  • Get selected every time Fizzgun receives a request with an Authorization header.
  • Create a mutant of the request in which the Authorization header is removed.
  • Create more mutants if the authorization type is basic. The mutants will get the header's value replaced by invalid values.
  • Optionally receive via configuration a list of invalid credentials that should have no access.
  • Expect the server response to be Bad Request, Unauthorized, or Forbidden.

Our AuthorizationVerifier Bubble

We'll start by creating a my_bubbles/auth_verifier.py file with the following content:

from fizzgun.bubbles import Bubble


class AuthorizationVerifier(Bubble):
    """Verifies access is denied for requests with invalid authorization settings"""

    def generate(self, request):
        pass

We'll shed some light on what we have here so far:

  • Bubbles must extend (directly or indirectly) from Fizzgun's Bubble class:
  • The class name (AuthorizationVerifier) will be used as the bubble name (for configuration and reporting purposes).
  • The class doc string will be used as bubble description (for reporting purposes).
  • All bubbles must implement the generate method. This method receives the original request and is responsible of yielding zero or more mutated requests. As we'll see later, other methods might be overridden but generate is the only one that is mandatory.

The request parameter passed to generate is an instance of fizzgun.models.HttpRequest. This object wraps Fizzgun's dict representation of an HTTP request and provides some extra utilities around it. The specification of this object is fully described in the appendix in the HttpRequest section.

You can take advantage of python type hints to get hints on the request argument:

from fizzgun.bubbles import Bubble
from fizzgun.models import HttpRequest

class AuthorizationVerifier(Bubble):
    """Verifies access is denied for requests with invalid authorization settings"""

    def generate(self, request: HttpRequest):
        pass

We'll leave the generate method for later, we'll override another method: does_apply.

The does_apply method

from fizzgun.bubbles import Bubble
from fizzgun.models import HttpRequest

class AuthorizationVerifier(Bubble):
    """Verifies access is denied for requests with invalid authorization settings"""

    def does_apply(self, request: HttpRequest) -> bool:
        return request.has_header('Authorization')

    def generate(self, request: HttpRequest):
        pass

Via does_apply we tell Fizzgun whether it should invoke the bubble's generate method or not for a given request. In this case generate will be invoked only for requests that have an authorization header (the default implementation always returns True). Instead, we could've just inspected the request in the generate method and decide not to yield any mutants but your code will be clearer if you separate these concerns.

The request object passed as an argument is the same than the one passed to generate ( HttpRequest).

Note: The has_header helper function uses the given header name argument in a case insensitive fashion so it will work even if the actual header in the original request is authorization or AUTHORIZATION.

Defining the response expectation

At the moment our bubble inherits Fizzgun default expectations: any server response to our mutants with a 5XX status code will be logged in the reports. However, since our bubble messes up with authorization we want any response that is not a bad request or an authorization error (e.g. a 200 OK response) to be logged as an error in the report:

from fizzgun.bubbles import Bubble
from fizzgun.models import HttpRequest

class AuthorizationVerifier(Bubble):
    """Verifies access is denied for requests with invalid authorization settings"""

    def initialize(self, *args, **kwargs):
        super(AuthorizationVerifier, self).initialize(*args, **kwargs)
        self.expectations.expect('status').to.be_in(400, 401, 403)

    def does_apply(self, request: HttpRequest) -> bool:
        return request.has_header('Authorization')

    def generate(self, request: HttpRequest):
        pass

We've now overridden the initialize method. You should not define a class constructor (i.e. an __init__ method), but you can hook on the bubble instantiation by overriding the initialize method. The first thing we do here is calling the super class' initialize so we get the default expectations and other configuration set up. Then we add a new response expectation.

You can set assertions on any property of a response, such as status code, body, or headers. You could expect the body to contain a substring (or not to contain it), to match with a particular regular expression, etc. Expectations are fully described in response expectations section of the appendix.

Generating mutants

We've been procastinating for too long, it's time to write that generate method and create some mutants.

Let's start producing a first mutant: We'll yield a copy of the original request but with the Authorization header removed:

from fizzgun.bubbles import Bubble
from fizzgun.models import HttpRequest
from fizzgun.models import HttpRequestBuilder

class AuthorizationVerifier(Bubble):
    """Verifies access is denied for requests with invalid authorization settings"""

    def initialize(self, *args, **kwargs):
        super(AuthorizationVerifier, self).initialize(*args, **kwargs)
        self.expectations.expect('status').to.be_in(400, 401, 403)

    def does_apply(self, request: HttpRequest) -> bool:
        return request.has_header('Authorization')

    def generate(self, request: HttpRequest):
        mutant = HttpRequestBuilder.new_from(request.value).without_header('Authorization').build()
        yield mutant

We've introduced a few new concepts:

  • The generate method should be either a python generator yielding mutants or it should return an iterable (e.g. a list) of mutants. In the example we could rewrite that last line as return [mutant].
  • The type of the yielded mutants it's not HttpRequest but Fizzgun's dict representation. However, we are using an HttpRequestBuilder utility class to create this mutant as a dictionary. This serves two purposes:
    • Let our code be more descriptive (instead of having to find and remove a header tuple from a list in a headers property of a dictionary).
    • Decoupling from the internal request representation since it might change in the future.
  • The HttpRequestBuilder is created with an initial state that we take from the original request request.value then we remove a header, and finally we build the new request dict representation. Refer to the HttpRequestBuilder section in the appendix to learn more about this class.

Let's now extend the generate method to produce other kind of mutants whenever the authorization type is basic:

import base64

from fizzgun.bubbles import Bubble
from fizzgun.models import HttpRequest
from fizzgun.models import HttpRequestBuilder

class AuthorizationVerifier(Bubble):
    """Verifies access is denied for requests with invalid authorization settings"""

    def initialize(self, *args, **kwargs):
        super(AuthorizationVerifier, self).initialize(*args, **kwargs)
        self.expectations.expect('status').to.be_in(400, 401, 403)

    def does_apply(self, request: HttpRequest) -> bool:
        return request.has_header('Authorization')

    def generate(self, request: HttpRequest):
        request_builder = HttpRequestBuilder.new_from(request.value)

        yield request_builder.without_header('Authorization').build() # The first mutant with the auth header removed

        if 'Basic' in request.headers['authorization']:
            invalid_payloads = [':', '', '@#$%^&*', ':::::']
            for auth_value in invalid_payloads:
                yield request_builder.with_header('Authorization', 'Basic ' + base64.b64encode(auth_value)).build()

Our bubble now generates one mutant for any request with an Authorization header, plus 4 additional mutants if the authorization type is basic.

Note: Keep in mind that HttpRequestBuilder is immutable, i.e. with every with* invocation you'll get a new instance.

Accepting initialization arguments

Finally we are going to allow users of our bubble to configure what basic auth values should result in access denied responses (or we leave our 4 invalid payloads as a default). For this purpose we are going to define a new invalid_basic_auth_credentials keyword argument in our initialize method:

import base64

from fizzgun.bubbles import Bubble
from fizzgun.models import HttpRequest
from fizzgun.models import HttpRequestBuilder

class AuthorizationVerifier(Bubble):
    """Verifies access is denied for requests with invalid authorization settings"""

    def initialize(self, invalid_basic_auth_credentials=None, *args, **kwargs):
        super(AuthorizationVerifier, self).initialize(*args, **kwargs)
        self.expectations.expect('status').to.be_in(400, 401, 403)
        self._invalid_basic_auth_credentials = invalid_basic_auth_credentials or [':', '', '@#$%^&*', ':::::']

    def does_apply(self, request: HttpRequest) -> bool:
        return request.has_header('Authorization')

    def generate(self, request: HttpRequest):
        request_builder = HttpRequestBuilder.new_from(request.value)

        yield request_builder.without_header('Authorization').build() # The first mutant with the auth header removed

        if 'Basic' in request.headers['authorization']:
            for auth_value in self._invalid_basic_auth_credentials:
                yield request_builder.with_header('Authorization', 'Basic ' + base64.b64encode(auth_value)).build()

We'll see later how this value can be set from a config file.

Our brand new bubble is ready to use, but before loading it we'll create some tags for it.

Tagging bubbles

Tagging bubbles is optional, but is recommended since it allow users to whitelist/blacklist your bubble via configuration.

Tags are defined as list of strings in a TAGS class attribute in your bubble:

import base64

from fizzgun.bubbles import Bubble
from fizzgun.models import HttpRequest
from fizzgun.models import HttpRequestBuilder

class AuthorizationVerifier(Bubble):
    """Verifies access is denied for requests with invalid authorization settings"""

    TAGS = ['name:authorization-verifier', 'category:security', 'data:headers']

    def initialize(self, invalid_basic_auth_credentials=None, *args, **kwargs):
        super(AuthorizationVerifier, self).initialize(*args, **kwargs)
        self.expectations.expect('status').to.be_in(400, 401, 403)
        self._invalid_basic_auth_credentials = invalid_basic_auth_credentials or [':', '', '@#$%^&*', ':::::']

    def does_apply(self, request: HttpRequest) -> bool:
        return request.has_header('Authorization')

    def generate(self, request: HttpRequest):
        request_builder = HttpRequestBuilder.new_from(request.value)

        yield request_builder.without_header('Authorization').build() # The first mutant with the auth header removed

        if 'Basic' in request.headers['authorization']:
            for auth_value in self._invalid_basic_auth_credentials:
                yield request_builder.with_header('Authorization', 'Basic ' + base64.b64encode(auth_value)).build()

Tags can be anything you want, however is recommended to follow some guidelines, for instance:

  • having a tag that uniquely identifies your bubble. E.g. 'name:authorization-verifier'.
  • adding a few more tags that include your bubble in a category (shared with other bubbles) so users can easily whitelist/blacklist a whole group of bubbles. E.g. 'category:security'.

Now let's load our AuthorizationVerifier bubble into Fizzgun!

Loading our custom bubble

Our bubble resides in my_bubbles/auth_verifier.py. We'll create now a bubble-pack module called my_bubbles that contains our AuthorizationVerifier bubble. Create a my_bubbles/__init__.py file with this content

from my_bubbles.auth_verifier import AuthenticationVerifier

BUBBLES = [AuthenticationVerifier]

For Fizzgun to be able to load custom bubbles it needs to have access to a module, importable by it's name, which contains a BUBBLES attribute specifying a list of Bubble classes (with only one bubble in this case).

We are not going to discuss here what a Python module is or how import works, your module just need to be accessible from the python paths whether it is installed as a site-package or, as in this case, it resides in the current working directory.

Now cd to your project root directory (the parent directory of my_bubbles/) and generate a fizzgun config file with the default settings:

fizzgun gen-config --defaults

A fizzgun.yaml file will be created. We are going to edit it and add a new - module: my_bubbles entry to the bubble-packs property.

bubbles:
  tags-blacklist: []
  tags-whitelist: []

  default-settings:
    expected_status_range: '0-499'
    mark_requests: false

  bubble-packs:
  - module: fizzgun.bubbles
  - module: my_bubbles

...

We can verify our bubble will get loaded by executing the fizzgun bubbles command:

$ fizzgun bubbles

...

Name: AuthenticationVerifier
Description: Verifies access is denied for requests with invalid authentication settings
Tags:
  * name:authorization-verifier
  * category:security
  * data:headers
Expectations:
  * Expecting 'status' to be in ranges ['0-499']
  * Expecting 'status' to be in [400, 401, 403]

Earlier in this tutorial we allowed our bubble to get an invalid_basic_auth_credentials configuration setting. You can override that setting now in the fizzgun.yaml config file if you wish:

bubbles:
  tags-blacklist: []
  tags-whitelist: []

  default-settings:
    expected_status_range: '0-499'
    mark_requests: false

  bubble-packs:
  - module: fizzgun.bubbles
  - module: my_bubbles
    settings:
      AuthenticationVerifier:
          invalid_basic_auth_credentials:
            - 'deleted-user:password' # user that was removed
            - ''  # empty auth
            - 'suspended-user:abcd123'
            - 'admin:amdin' 
...

That's all! now execute fizzgun run and start catching new authorization bugs in your service 🤘.