Skip to content

Django: cleanup & extract cookies from request #417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions aikido_zen/sources/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
def _get_response_before(func, instance, args, kwargs):
request = get_argument(args, kwargs, 0, "request")

run_init_stage(request)

if pre_response_middleware not in instance._view_middleware:
# The rate limiting middleware needs to be last in the chain.
instance._view_middleware += [pre_response_middleware]

run_init_stage(request)

Check warning on line 19 in aikido_zen/sources/django/__init__.py

View check run for this annotation

Codecov / codecov/patch

aikido_zen/sources/django/__init__.py#L18-L19

Added lines #L18 - L19 were not covered by tests

@after
def _get_response_after(func, instance, args, kwargs, return_value):
Expand Down
60 changes: 16 additions & 44 deletions aikido_zen/sources/django/run_init_stage.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,26 @@
"""Exports run_init_stage function"""

import json
from aikido_zen.context import Context
from aikido_zen.helpers.logging import logger
from .try_extract_body import try_extract_body_from_django_request
from .try_extract_cookies import try_extract_cookies_from_django_request
from ..functions.request_handler import request_handler


def run_init_stage(request):
"""Parse request and body, run "init" stage with request_handler"""
body = None
try:
# try-catch loading of form parameters, this is to fix issue with DATA_UPLOAD_MAX_NUMBER_FIELDS :
try:
body = request.POST.dict()
if len(body) == 0:
body = None # Reset
except Exception:
pass
context = None
if (
hasattr(request, "scope") and request.scope is not None
): # This request is an ASGI request
context = Context(req=request.scope, source="django_async")
elif hasattr(request, "META") and request.META is not None: # WSGI request
context = Context(req=request.META, source="django")
else:
return

# Check for JSON or XML :
if body is None and request.content_type == "application/json":
try:
body = json.loads(request.body)
except Exception:
pass
if body is None or len(body) == 0:
# E.g. XML Data
body = request.body
if body is None or len(body) == 0:
# During a GET request, django leaves the body as an empty byte string (e.g. `b''`).
# When an attack is detected, this body needs to be serialized which would fail.
# So a byte string gets converted into a string to stop that from happening.
body = "" # Set body to an empty string.
except Exception as e:
logger.debug("Exception occurred in run_init_stage function (Django) : %s", e)
# Parse some attributes separately
context.set_body(try_extract_body_from_django_request(request))
context.cookies = try_extract_cookies_from_django_request(request)

# In a separate try-catch we set the context :
try:
context = None
if (
hasattr(request, "scope") and request.scope is not None
): # This request is an ASGI request
context = Context(req=request.scope, body=body, source="django_async")
elif hasattr(request, "META") and request.META is not None: # WSGI request
context = Context(req=request.META, body=body, source="django")
else:
return
context.set_as_current_context()

# Init stage needs to be run with context already set :
request_handler(stage="init")
except Exception as e:
logger.debug("Exception occurred in run_init_stage function (Django): %s", e)
context.set_as_current_context()
request_handler(stage="init")
32 changes: 32 additions & 0 deletions aikido_zen/sources/django/try_extract_body.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import json
from aikido_zen.helpers.logging import logger


def try_extract_body_from_django_request(request):
body = None
try:
# try-catch loading of form parameters, this is to fix issue with DATA_UPLOAD_MAX_NUMBER_FIELDS :
try:
body = request.POST.dict()
if len(body) == 0:
body = None # Reset
except Exception:
pass

# Check for JSON or XML :
if body is None and request.content_type == "application/json":
try:
body = json.loads(request.body)
except Exception:
pass
if body is None or len(body) == 0:
# E.g. XML Data
body = request.body
if body is None or len(body) == 0:
# During a GET request, django leaves the body as an empty byte string (e.g. `b''`).
# When an attack is detected, this body needs to be serialized which would fail.
# So a byte string gets converted into a string to stop that from happening.
return "" # Set body to an empty string.
except Exception as e:
logger.debug("Exception occurred trying to extract django body: %s", e)

Check warning on line 31 in aikido_zen/sources/django/try_extract_body.py

View check run for this annotation

Codecov / codecov/patch

aikido_zen/sources/django/try_extract_body.py#L30-L31

Added lines #L30 - L31 were not covered by tests
return body
11 changes: 11 additions & 0 deletions aikido_zen/sources/django/try_extract_cookies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from aikido_zen.helpers.logging import logger


def try_extract_cookies_from_django_request(request):
try:
# https://github.com/django/django/blob/7091801e046dc85dba2238ed4eaf0b3f62bcfc7f/django/core/handlers/wsgi.py#L100
# https://github.com/django/django/blob/7091801e046dc85dba2238ed4eaf0b3f62bcfc7f/django/core/handlers/asgi.py#L131
cookies = request.COOKIES
return cookies
except Exception as e:
logger.debug("Exception occurred trying to extract django cookies: %s", e)

Check warning on line 11 in aikido_zen/sources/django/try_extract_cookies.py

View check run for this annotation

Codecov / codecov/patch

aikido_zen/sources/django/try_extract_cookies.py#L10-L11

Added lines #L10 - L11 were not covered by tests
29 changes: 28 additions & 1 deletion end2end/django_postgres_gunicorn_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import pytest
import requests
import time
from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type
from .server.check_events_from_mock import fetch_events_from_mock, validate_started_event, filter_on_event_type, \
clear_events_from_mock

# e2e tests for django_postgres_gunicorn sample app
post_url_fw = "http://localhost:8100/app/create"
Expand All @@ -26,6 +27,7 @@ def test_safe_response_without_firewall():


def test_dangerous_response_with_firewall():
clear_events_from_mock("http://localhost:5000")
dog_name = "Dangerous bobby', TRUE); -- "
res = requests.post(post_url_fw, data={'dog_name': dog_name})
assert res.status_code == 500
Expand All @@ -50,6 +52,31 @@ def test_dangerous_response_with_firewall():
'user': None
}


def test_dangerous_response_with_firewall():
clear_events_from_mock("http://localhost:5000")
cookie_header = "dog_name=Dangerous bobby', TRUE) --; ,2=2"
res = requests.get(f"{post_url_fw}/via_cookies", headers={
"Cookie": cookie_header
})
assert res.status_code == 500

time.sleep(5) # Wait for attack to be reported
events = fetch_events_from_mock("http://localhost:5000")
attacks = filter_on_event_type(events, "detected_attack")

assert len(attacks) == 1
assert attacks[0]["attack"]["blocked"]
assert attacks[0]["attack"]["kind"] == "sql_injection"
assert attacks[0]["attack"]["metadata"] == {
'dialect': "postgres",
'sql': "INSERT INTO sample_app_Dogs (dog_name, is_admin) VALUES ('Dangerous bobby', TRUE) --', FALSE)"
}
assert attacks[0]["attack"]["pathToPayload"] == ".dog_name"
assert attacks[0]["attack"]["source"] == "cookies"
assert attacks[0]["attack"]["payload"] == "\"Dangerous bobby', TRUE) --\""


def test_dangerous_response_without_firewall():
dog_name = "Dangerous bobby', TRUE); -- "
res = requests.post(post_url_nofw, data={'dog_name': dog_name})
Expand Down
6 changes: 6 additions & 0 deletions end2end/server/check_events_from_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ def fetch_events_from_mock(url):
json_events = json.loads(res.content.decode("utf-8"))
return json_events

def clear_events_from_mock(url):
mock_events_url = f"{url}/mock/reset"
res = requests.get(mock_events_url, timeout=5)
return json.loads(res.content.decode("utf-8"))


def filter_on_event_type(events, type):
return [event for event in events if event["type"] == type]

Expand Down
6 changes: 6 additions & 0 deletions end2end/server/mock_aikido_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ def mock_get_events():
return jsonify(events)


@app.route('/mock/reset', methods=['GET'])
def mock_get_events():
events.clear()
return jsonify({"msg": "OK"})


if __name__ == '__main__':
if len(sys.argv) < 2 or len(sys.argv) > 3:
print("Usage: python mock_server.py <port> [config_file]")
Expand Down
3 changes: 2 additions & 1 deletion sample-apps/django-postgres-gunicorn/sample_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
urlpatterns = [
path("", views.index, name="index"),
path("dogpage/<int:dog_id>", views.dog_page, name="dog_page"),
path("create", views.create_dogpage, name="create")
path("create", views.create_dogpage, name="create"),
path("create/via_cookies", views.create_dogpage_cookies, name="create_cookies")
]
11 changes: 11 additions & 0 deletions sample-apps/django-postgres-gunicorn/sample_app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,14 @@ def create_dogpage(request):
cursor.execute(query)

return HttpResponse("Dog page created")

@csrf_exempt
def create_dogpage_cookies(request):
dog_name = request.COOKIES.get('dog_name')
# Using custom sql to create a dog :
with connection.cursor() as cursor:
query = f"INSERT INTO sample_app_Dogs (dog_name, is_admin) VALUES ('%s', FALSE)" % (dog_name)
print("QUERY : ", query)
cursor.execute(query)

return HttpResponse("Dog page created")
Loading