Created
February 10, 2021 10:15
-
-
Save mdales/e57cde9134d82e807f7d31a3fbc64043 to your computer and use it in GitHub Desktop.
My test script for using Atheris to fuzz a Django app
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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