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
Authorizationheader. - Create a mutant of the request in which the
Authorizationheader 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, orForbidden.
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
Bubbleclass: - 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
generatemethod. 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 butgenerateis 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
generatemethod 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 asreturn [mutant]. - The type of the yielded mutants it's not
HttpRequestbut Fizzgun's dict representation. However, we are using anHttpRequestBuilderutility 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
headersproperty of a dictionary). - Decoupling from the internal request representation since it might change in the future.
- Let our code be more descriptive (instead of having to find and remove a header tuple from a list in a
- The
HttpRequestBuilderis created with an initial state that we take from the original requestrequest.valuethen 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 🤘.