Skip to content

Instantly share code, notes, and snippets.

@mdales
Created February 10, 2021 10:15
Show Gist options
  • Save mdales/e57cde9134d82e807f7d31a3fbc64043 to your computer and use it in GitHub Desktop.
Save mdales/e57cde9134d82e807f7d31a3fbc64043 to your computer and use it in GitHub Desktop.
My test script for using Atheris to fuzz a Django app
#!/usr/bin/env python
import sys
import atheris
import django
from django.test import Client
from django.urls import URLPattern, URLResolver, reverse, NoReverseMatch
from django.urls.resolvers import RegexPattern, RoutePattern
from django.urls.converters import StringConverter, IntConverter
# Start Django before we access anything else
django.setup()
from urls import urlpatterns # noqa
# List of apps whose URLpatterns we wish to exclude from testing
IGNORE_APPS = ['djdt','admin']
def flatten_urls(namespace, patterns):
"""Recursive call to traverse Django's urlpatterns and turn it into a list of patterns."""
res = []
for url in patterns:
if isinstance(url, URLPattern):
res.append((namespace, url))
elif isinstance(url, URLResolver):
app_name = None
try:
# For URLResolver
app_name = url.app_name
except AttributeError:
try:
app_name = url.urlconf_module.app_name
except AttributeError:
pass
if app_name not in IGNORE_APPS:
res += flatten_urls(url.namespace, url.url_patterns)
return res
test_urls = flatten_urls(None, urlpatterns)
print("Testing the following URL patterns:")
for x in test_urls:
print(f"\t{x}")
def test_my_url(data):
"""Atheris test method that will try to invoke a single URL at a time from the list we built"""
fdp = atheris.FuzzedDataProvider(data)
namespace, urlpattern = fdp.PickValueInList(test_urls)
method = fdp.PickValueInList(["GET", "POST", "PUT", "HEAD", "DELETE"])
data_length = fdp.ConsumeUInt(3)
name = urlpattern.name
if namespace is not None:
name = f"{namespace}:{name}"
url = None
pattern = urlpattern.pattern
if isinstance(pattern, RegexPattern):
# Don't try to be smart for regex patterns, just skip over them if
# they don't have a straight match
try:
url = reverse(name)
except NoReverseMatch:
return
elif isinstance(pattern, RoutePattern):
# With router pattern we can try used named arguments
kwargs = {}
for key in pattern.converters:
if isinstance(pattern.converters[key], StringConverter):
# strlen of 0 will be failed by reverse
str_length = fdp.ConsumeUInt(3) + 1
kwargs[key] = fdp.ConsumeUnicode(str_length)
elif isinstance(pattern.converters[key], IntConverter):
kwargs[key] = fdp.ConsumeInt(3)
else:
raise ValueError(f"unexpected converter for {key} in {name}")
try:
url = reverse(name, kwargs=kwargs)
except NoReverseMatch:
# This can happen if string args are '' for instance
return
except UnicodeEncodeError:
# Not all data returned by FuzzDataProvider.ConsumeUnicode is necessarily
# valid unicode as they try to test unicode parsing, but here it just
# causes us to fail in reverse, so skip it.
return
assert url
client = Client()
response = None
if method == "GET":
response = client.get(url + "?" + fdp.ConsumeString(data_length))
elif method == "HEAD":
response = client.head(url + "?" + fdp.ConsumeString(data_length))
elif method == "POST":
response = client.post(url, fdp.ConsumeBytes(data_length), content_type="application/binary")
elif method == "PUT":
response = client.put(url, fdp.ConsumeBytes(data_length), content_type="application/binary")
elif method == "DELETE":
response = client.delete(url + "?" + fdp.ConsumeString(data_length))
assert response
# If we return an unexpected error code for our application fail
if response.status_code not in [200, 301, 302, 400, 401, 403, 404, 405]:
raise RuntimeError(f"Got {response.status_code} response for {url}")
atheris.Setup(sys.argv, test_my_url)
atheris.Fuzz()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment