diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 000000000..69cb76019 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1 @@ +comment: false diff --git a/aikido_firewall/__init__.py b/aikido_firewall/__init__.py index b4274ea9f..292c8aed9 100644 --- a/aikido_firewall/__init__.py +++ b/aikido_firewall/__init__.py @@ -4,20 +4,25 @@ from dotenv import load_dotenv -# Import sources -import aikido_firewall.sources.django -import aikido_firewall.sources.flask - -# Import sinks -import aikido_firewall.sinks.pymysql - -# Import middleware -import aikido_firewall.middleware.django # Import logger from aikido_firewall.helpers.logging import logger +# Import agent +from aikido_firewall.agent import start_agent + # Load environment variables load_dotenv() -logger.info("Aikido python firewall started") + +def protect(): + """Start Aikido agent""" + # Import sources + import aikido_firewall.sources.django + import aikido_firewall.sources.flask + + # Import sinks + import aikido_firewall.sinks.pymysql + + logger.info("Aikido python firewall started") + start_agent() diff --git a/aikido_firewall/agent/__init__.py b/aikido_firewall/agent/__init__.py new file mode 100644 index 000000000..418f0172b --- /dev/null +++ b/aikido_firewall/agent/__init__.py @@ -0,0 +1,74 @@ +""" +Aikido agent, this will create a new thread and listen for stuff sent by our sources and sinks +""" + +import time +import queue +from threading import Thread +from aikido_firewall.helpers.logging import logger + +AGENT_SEC_INTERVAL = 60 + + +class AikidoThread: + """ + Our agent thread + """ + + def __init__(self, q): + logger.debug("Agent thread started") + while True: + while not q.empty(): + self.process_data(q.get()) + time.sleep(AGENT_SEC_INTERVAL) + self.q = q + self.current_context = None + + def process_data(self, item): + """Will process the data added to the queue""" + action, data = item + logger.debug("Action %s, Data %s", action, data) + if action == "REPORT": + logger.debug("Report") + self.current_context = data + else: + logger.error("Action `%s` is not defined. (Aikido Agent)", action) + + +# pylint: disable=invalid-name # This variable does change +agent = None + + +def get_agent(): + """Returns the globally stored agent""" + return agent + + +def start_agent(): + """ + Starts a thread to handle incoming/outgoing data + """ + # pylint: disable=global-statement # We need this to be global + global agent + + # This creates a queue for Inter-Process Communication + logger.debug("Creating IPC Queue") + q = queue.Queue() + + logger.debug("Starting a new agent thread") + agent_thread = Thread(target=AikidoThread, args=(q,)) + agent_thread.start() + agent = Agent(q) + + +class Agent: + """Agent class""" + + def __init__(self, q): + self.q = q + + def report(self, obj, action): + """ + Report something to the agent + """ + self.q.put((action, obj)) diff --git a/aikido_firewall/context/__init__.py b/aikido_firewall/context/__init__.py new file mode 100644 index 000000000..202e565b7 --- /dev/null +++ b/aikido_firewall/context/__init__.py @@ -0,0 +1,50 @@ +""" +Provides all the functionality for contexts +""" + +import threading + +local = threading.local() + + +def get_current_context(): + """Returns the current context""" + return local.current_context + + +class Context: + """ + A context object, it stores everything that is important + for vulnerability detection + """ + + def __init__(self, req): + self.method = req.method + self.remote_address = req.remote_addr + self.url = req.url + self.body = req.form + self.headers = req.headers + self.query = req.args + self.cookies = req.cookies + self.source = "flask" + + def __reduce__(self): + return ( + self.__class__, + ( + self.method, + self.remote_address, + self.url, + self.body, + self.headers, + self.query, + self.cookies, + self.source, + ), + ) + + def set_as_current_context(self): + """ + Set the current context + """ + local.current_context = self diff --git a/aikido_firewall/helpers/build_route_from_url.py b/aikido_firewall/helpers/build_route_from_url.py new file mode 100644 index 000000000..9b0dc9c3e --- /dev/null +++ b/aikido_firewall/helpers/build_route_from_url.py @@ -0,0 +1,82 @@ +""" +Module with logic to build a route, i.e. find route params +from a simple URL string +""" + +import re +import ipaddress +from aikido_firewall.helpers.looks_like_a_secret import looks_like_a_secret +from aikido_firewall.helpers.try_parse_url_path import try_parse_url_path + +UUID_REGEX = re.compile( + r"(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + re.I, +) +NUMBER_REGEX = re.compile(r"^\d+$") +DATE_REGEX = re.compile(r"^\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4}$") +EMAIL_REGEX = re.compile( + r"^[a-zA-Z0-9.!#$%&\'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" +) +HASH_REGEX = re.compile( + r"^(?:[a-f0-9]{32}|[a-f0-9]{40}|[a-f0-9]{64}|[a-f0-9]{128})$", re.I +) +HASH_LENGTHS = [32, 40, 64, 128] + + +def build_route_from_url(url): + """ + Main helper function which will build the route + from a URL string as input + """ + path = try_parse_url_path(url) + + if not path: + return None + + route = "/".join( + [replace_url_segment_with_param(segment) for segment in path.split("/")] + ) + + if route == "/": + return "/" + + if route.endswith("/"): + return route[:-1] + + return route + + +def replace_url_segment_with_param(segment): + """ + ?????????? + """ + if not segment: # Check if segment is empty + return segment # Return the segment as is if it's empty + char_code = ord(segment[0]) + starts_with_number = 48 <= char_code <= 57 # ASCII codes for '0' to '9' + + if starts_with_number and NUMBER_REGEX.match(segment): + return ":number" + + if len(segment) == 36 and UUID_REGEX.match(segment): + return ":uuid" + + if starts_with_number and DATE_REGEX.match(segment): + return ":date" + + if "@" in segment and EMAIL_REGEX.match(segment): + return ":email" + + try: + ipaddress.ip_address(segment) + return ":ip" + except ValueError: + pass + + if len(segment) in HASH_LENGTHS and HASH_REGEX.match(segment): + return ":hash" + + if looks_like_a_secret(segment): + return ":secret" + + return segment diff --git a/aikido_firewall/helpers/build_route_from_url_test.py b/aikido_firewall/helpers/build_route_from_url_test.py new file mode 100644 index 000000000..da02028cb --- /dev/null +++ b/aikido_firewall/helpers/build_route_from_url_test.py @@ -0,0 +1,116 @@ +from aikido_firewall.helpers.build_route_from_url import build_route_from_url +import pytest +import hashlib + + +def generate_hash(algorithm): + data = "test" + if algorithm == "md5": + return hashlib.md5(data.encode()).hexdigest() + elif algorithm == "sha1": + return hashlib.sha1(data.encode()).hexdigest() + elif algorithm == "sha256": + return hashlib.sha256(data.encode()).hexdigest() + elif algorithm == "sha512": + return hashlib.sha512(data.encode()).hexdigest() + else: + return None + + +def test_invalid_urls(): + assert build_route_from_url("") == None + assert build_route_from_url("http") == None + + +def test_root_urls(): + assert build_route_from_url("/") == "/" + assert build_route_from_url("http://localhost/") == "/" + + +def test_replace_numbers(): + assert build_route_from_url("/posts/3") == "/posts/:number" + assert build_route_from_url("http://localhost/posts/3") == "/posts/:number" + assert build_route_from_url("http://localhost/posts/3/") == "/posts/:number" + assert ( + build_route_from_url("http://localhost/posts/3/comments/10") + == "/posts/:number/comments/:number" + ) + assert ( + build_route_from_url("/blog/2023/05/great-article") + == "/blog/:number/:number/great-article" + ) + + +def test_replace_dates(): + assert build_route_from_url("/posts/2023-05-01") == "/posts/:date" + assert build_route_from_url("/posts/2023-05-01/") == "/posts/:date" + assert ( + build_route_from_url("/posts/2023-05-01/comments/2023-05-01") + == "/posts/:date/comments/:date" + ) + assert build_route_from_url("/posts/01-05-2023") == "/posts/:date" + + +def test_ignore_comma_numbers(): + assert build_route_from_url("/posts/3,000") == "/posts/3,000" + + +def test_ignore_api_version_numbers(): + assert build_route_from_url("/v1/posts/3") == "/v1/posts/:number" + + +def test_replace_uuids(): + uuids = [ + "d9428888-122b-11e1-b85c-61cd3cbb3210", + "000003e8-2363-21ef-b200-325096b39f47", + "a981a0c2-68b1-35dc-bcfc-296e52ab01ec", + "109156be-c4fb-41ea-b1b4-efe1671c5836", + "90123e1c-7512-523e-bb28-76fab9f2f73d", + "1ef21d2f-1207-6660-8c4f-419efbd44d48", + "017f22e2-79b0-7cc3-98c4-dc0c0c07398f", + "0d8f23a0-697f-83ae-802e-48f3756dd581", + ] + for uuid in uuids: + assert build_route_from_url(f"/posts/{uuid}") == "/posts/:uuid" + + +def test_ignore_invalid_uuids(): + assert ( + build_route_from_url("/posts/00000000-0000-1000-6000-000000000000") + == "/posts/00000000-0000-1000-6000-000000000000" + ) + + +def test_ignore_strings(): + assert build_route_from_url("/posts/abc") == "/posts/abc" + + +def test_replace_email_addresses(): + assert build_route_from_url("/login/john.doe@acme.com") == "/login/:email" + assert build_route_from_url("/login/john.doe+alias@acme.com") == "/login/:email" + + +def test_replace_ip_addresses(): + assert build_route_from_url("/block/1.2.3.4") == "/block/:ip" + assert ( + build_route_from_url("/block/2001:2:ffff:ffff:ffff:ffff:ffff:ffff") + == "/block/:ip" + ) + assert build_route_from_url("/block/64:ff9a::255.255.255.255") == "/block/:ip" + assert build_route_from_url("/block/100::") == "/block/:ip" + assert build_route_from_url("/block/fec0::") == "/block/:ip" + assert build_route_from_url("/block/227.202.96.196") == "/block/:ip" + + +def test_replace_hashes(): + assert build_route_from_url(f"/files/{generate_hash('md5')}") == "/files/:hash" + assert build_route_from_url(f"/files/{generate_hash('sha1')}") == "/files/:hash" + assert build_route_from_url(f"/files/{generate_hash('sha256')}") == "/files/:hash" + assert build_route_from_url(f"/files/{generate_hash('sha512')}") == "/files/:hash" + + +def test_replace_secrets(): + assert ( + build_route_from_url("/confirm/CnJ4DunhYfv2db6T1FRfciRBHtlNKOYrjoz") + == "/confirm/:secret" + ) diff --git a/aikido_firewall/helpers/looks_like_a_secret.py b/aikido_firewall/helpers/looks_like_a_secret.py new file mode 100644 index 000000000..f0ce18174 --- /dev/null +++ b/aikido_firewall/helpers/looks_like_a_secret.py @@ -0,0 +1,51 @@ +""" +Helper function file, see function doc for explanation +""" + +import string + +LOWERCASE = list(string.ascii_lowercase) +UPPERCASE = list(map(str.upper, LOWERCASE)) +NUMBERS = list(string.digits) +SPECIAL = list("!#$%^&*|;:<>") +KNOWN_WORD_SEPARATORS = ["-"] +WHITE_SPACE = " " +MINIMUM_LENGTH = 10 + + +def looks_like_a_secret(s): + """ + This will make a judgement based on the string s wether + or not s looks like a secret + """ + if len(s) <= MINIMUM_LENGTH: + return False + + has_number = any(char in s for char in NUMBERS) + if not has_number: + return False + + has_lower = any(char in s for char in LOWERCASE) + has_upper = any(char in s for char in UPPERCASE) + has_special = any(char in s for char in SPECIAL) + charsets = [has_lower, has_upper, has_special] + + if charsets.count(True) < 2: + return False + + if WHITE_SPACE in s: + return False + + if any(separator in s for separator in KNOWN_WORD_SEPARATORS): + return False + + window_size = MINIMUM_LENGTH + ratios = [] + for i in range(len(s) - window_size + 1): + window = s[i : i + window_size] + unique_chars = set(window) + ratios.append(len(unique_chars) / window_size) + + average_ratio = sum(ratios) / len(ratios) + + return average_ratio > 0.75 diff --git a/aikido_firewall/helpers/looks_like_a_secret_test.py b/aikido_firewall/helpers/looks_like_a_secret_test.py new file mode 100644 index 000000000..25070fe61 --- /dev/null +++ b/aikido_firewall/helpers/looks_like_a_secret_test.py @@ -0,0 +1,184 @@ +import pytest +import random +from aikido_firewall.helpers.looks_like_a_secret import looks_like_a_secret + +LOWERCASE = "abcdefghijklmnopqrstuvwxyz" +UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +NUMBERS = "0123456789" +SPECIALS = "!#$%^&*|;:<>" +MINIMUM_LENGTH = 10 + + +def secret_from_charset(length, charset): + return "".join(random.choice(charset) for _ in range(length)) + + +def test_empty_string(): + assert looks_like_a_secret("") == False + + +def test_short_strings(): + short_strings = [ + "c", + "NR", + "7t3", + "4qEK", + "KJr6s", + "KXiW4a", + "Fupm2Vi", + "jiGmyGfg", + "SJPLzVQ8t", + "OmNf04j6mU", + ] + for s in short_strings: + assert looks_like_a_secret(s) == False + + +def test_long_strings(): + assert looks_like_a_secret("rsVEExrR2sVDONyeWwND") == True + assert looks_like_a_secret(":2fbg;:qf$BRBc<2AG8&") == True + + +def test_very_long_strings(): + assert ( + looks_like_a_secret( + "efDJHhzvkytpXoMkFUgag6shWJktYZ5QUrUCTfecFELpdvaoAT3tekI4ZhpzbqLt" + ) + == True + ) + assert ( + looks_like_a_secret( + "XqSwF6ySwMdTomIdmgFWcMVXWf5L0oVvO5sIjaCPI7EjiPvRZhZGWx3A6mLl1HXPOHdUeabsjhngW06JiLhAchFwgtUaAYXLolZn75WsJVKHxEM1mEXhlmZepLCGwRAM" + ) + == True + ) + + +def test_contains_white_space(): + assert looks_like_a_secret("rsVEExrR2sVDONyeWwND ") == False + + +def test_less_than_2_charsets(): + assert looks_like_a_secret(secret_from_charset(10, LOWERCASE)) == False + assert looks_like_a_secret(secret_from_charset(10, UPPERCASE)) == False + assert looks_like_a_secret(secret_from_charset(10, NUMBERS)) == False + assert looks_like_a_secret(secret_from_charset(10, SPECIALS)) == False + + +def test_common_url_terms(): + url_terms = [ + "development", + "programming", + "applications", + "implementation", + "environment", + "technologies", + "documentation", + "demonstration", + "configuration", + "administrator", + "visualization", + "international", + "collaboration", + "opportunities", + "functionality", + "customization", + "specifications", + "optimization", + "contributions", + "accessibility", + "subscription", + "subscriptions", + "infrastructure", + "architecture", + "authentication", + "sustainability", + "notifications", + "announcements", + "recommendations", + "communication", + "compatibility", + "enhancement", + "integration", + "performance", + "improvements", + "introduction", + "capabilities", + "communities", + "credentials", + "integration", + "permissions", + "validation", + "serialization", + "deserialization", + "rate-limiting", + "throttling", + "load-balancer", + "microservices", + "endpoints", + "data-transfer", + "encryption", + "authorization", + "bearer-token", + "multipart", + "urlencoded", + "api-docs", + "postman", + "json-schema", + "serialization", + "deserialization", + "rate-limiting", + "throttling", + "load-balancer", + "api-gateway", + "microservices", + "endpoints", + "data-transfer", + "encryption", + "signature", + "poppins-bold-webfont.woff2", + "karla-bold-webfont.woff2", + "startEmailBasedLogin", + "jenkinsFile", + "ConnectionStrings.config", + "coach", + "login", + "payment_methods", + "activity_logs", + "feedback_responses", + "balance_transactions", + "customer_sessions", + "payment_intents", + "billing_portal", + "subscription_items", + "namedLayouts", + "PlatformAction", + "quickActions", + "queryLocator", + "relevantItems", + "parameterizedSearch", + ] + for term in url_terms: + assert looks_like_a_secret(term) == False + + +def test_known_word_separators(): + assert looks_like_a_secret("this-is-a-secret-1") == False + + +def test_number_is_not_a_secret(): + assert looks_like_a_secret("1234567890") == False + assert looks_like_a_secret("1234567890" * 2) == False + + +def test_known_secrets(): + secrets = [ + "yqHYTS=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "blinker" +version = "1.8.2" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.8" +files = [ + {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, + {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, +] + [[package]] name = "click" version = "8.1.7" @@ -159,6 +170,43 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] +[[package]] +name = "flask" +version = "3.0.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, + {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, +] + +[package.dependencies] +blinker = ">=1.6.2" +click = ">=8.1.3" +itsdangerous = ">=2.1.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.0.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-http-middleware" +version = "0.4.2" +description = "A module to create middleware with direct access to `request` and `response`" +optional = false +python-versions = "*" +files = [ + {file = "flask-http-middleware-0.4.2.tar.gz", hash = "sha256:ff9e1cdd8499a1e65013f1692d9e14abec360e4c21e8129068478ba820480e7d"}, + {file = "flask_http_middleware-0.4.2-py3-none-any.whl", hash = "sha256:3ba9860dfac8b13d77e18e896bd67e4b3eb8451d415db6e5292dba117f6b6fc3"}, +] + +[package.dependencies] +flask = "*" +werkzeug = "*" + [[package]] name = "importhook" version = "1.0.9" @@ -195,6 +243,103 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -374,7 +519,24 @@ files = [ {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, ] +[[package]] +name = "werkzeug" +version = "3.0.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, + {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "b9af78839e3f7cfc0cb8ce3475bdd2e4caa1b785182d461169c333fc769651d9" +content-hash = "94d48d4cdd270d7de265606c56fa5cd5c69076a84920c5071523b4be5128e955" diff --git a/pyproject.toml b/pyproject.toml index f97201734..ab78cdea0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,8 @@ python-dotenv = "^1.0.1" pytest = "^8.2.2" pytest-cov = "^5.0.0" pytest-mock = "^3.14.0" +werkzeug = "^3.0.3" +flask-http-middleware = "^0.4.2" [tool.poetry.group.dev.dependencies] black = "^24.4.2" diff --git a/sample-apps/flask-mysql/README.md b/sample-apps/flask-mysql/README.md index 80f139b62..2b295c444 100644 --- a/sample-apps/flask-mysql/README.md +++ b/sample-apps/flask-mysql/README.md @@ -6,4 +6,4 @@ docker-compose up --build - You'll be able to access the Flask Server at : [localhost:8080](http://localhost:8080) - To Create a reference test dog use `http://localhost:8080/create/doggo` -- To test a sql injection enter the following link : `http://localhost:8080/create/Malicious dog", 1); --%20` \ No newline at end of file +- To test a sql injection enter the following dog name : `Malicious dog", 1); -- ` diff --git a/sample-apps/flask-mysql/app.py b/sample-apps/flask-mysql/app.py index 2b80d2f6f..2322b0cef 100644 --- a/sample-apps/flask-mysql/app.py +++ b/sample-apps/flask-mysql/app.py @@ -1,9 +1,12 @@ import aikido_firewall # Aikido package import +aikido_firewall.protect() -from flask import Flask, render_template +from flask import Flask, render_template, request from flaskext.mysql import MySQL app = Flask(__name__) +if __name__ == '__main__': + app.run(threaded=True) # Run threaded so we can test our agent's capabilities mysql = MySQL() app.config['MYSQL_DATABASE_HOST'] = 'db' @@ -27,10 +30,15 @@ def get_dogpage(dog_id): dog = cursor.fetchmany(1)[0] return render_template('dogpage.html', title=f'Dog', dog=dog, isAdmin=("Yes" if dog[2] else "No")) -@app.route('/create/') -def create_dog(dog_name): +@app.route("/create", methods=['GET']) +def show_create_dog_form(): + return render_template('create_dog.html') + +@app.route("/create", methods=['POST']) +def create_dog(): + dog_name = request.form['dog_name'] connection = mysql.get_db() cursor = connection.cursor() - cursor.execute(f'INSERT INTO dogs (dog_name, isAdmin) VALUES("%s", 0)' % (dog_name)) + cursor.execute(f'INSERT INTO dogs (dog_name, isAdmin) VALUES ("%s", 0)' % (dog_name)) connection.commit() - return f'Dog {(dog_name)}' \ No newline at end of file + return f'Dog {dog_name} created successfully' diff --git a/sample-apps/flask-mysql/templates/create_dog.html b/sample-apps/flask-mysql/templates/create_dog.html new file mode 100644 index 000000000..fe26d428e --- /dev/null +++ b/sample-apps/flask-mysql/templates/create_dog.html @@ -0,0 +1,17 @@ + + + + + + + Create Dog + + +

Create a Dog

+
+ + + +
+ +