diff --git a/aikido_firewall/__init__.py b/aikido_firewall/__init__.py index 9b6feb4e5..10e86278b 100644 --- a/aikido_firewall/__init__.py +++ b/aikido_firewall/__init__.py @@ -40,6 +40,8 @@ def protect(module="any", server=True): import aikido_firewall.sinks.mysqlclient import aikido_firewall.sinks.pymongo import aikido_firewall.sinks.psycopg2 + import aikido_firewall.sinks.builtins + import aikido_firewall.sinks.os import aikido_firewall.sinks.http_client import aikido_firewall.sinks.socket diff --git a/aikido_firewall/errors/__init__.py b/aikido_firewall/errors/__init__.py index a8f211500..06d0d8207 100644 --- a/aikido_firewall/errors/__init__.py +++ b/aikido_firewall/errors/__init__.py @@ -23,5 +23,13 @@ def __init__(self, message="You are rate limited by Aikido firewall."): self.message = message +class AikidoPathTraversal(AikidoException): + """Exception because of a path traversal""" + + def __init__(self, message="This is a path traversal attack, halted by Aikido."): + super().__init__(self, message) + self.message = message + + class AikidoSSRF(AikidoException): """Exception because of SSRF""" diff --git a/aikido_firewall/sinks/builtins.py b/aikido_firewall/sinks/builtins.py new file mode 100644 index 000000000..3a451bcc3 --- /dev/null +++ b/aikido_firewall/sinks/builtins.py @@ -0,0 +1,47 @@ +""" +Sink module for `builtins`, python's built-in function +""" + +import copy +import importhook +from aikido_firewall.helpers.logging import logger +from aikido_firewall.vulnerabilities.path_traversal.check_context_for_path_traversal import ( + check_context_for_path_traversal, +) +from aikido_firewall.context import get_current_context +from aikido_firewall.background_process import get_comms +from aikido_firewall.errors import AikidoPathTraversal + + +@importhook.on_import("builtins") +def on_builtins_import(builtins): + """ + Hook 'n wrap on `builtins`, python's built-in functions + Our goal is to wrap the open() function, which you use when opening files + Returns : Modified builtins object + """ + modified_builtins = importhook.copy_module(builtins) + + former_open = copy.deepcopy(builtins.open) + + def aikido_new_open(*args, **kwargs): + logger.debug("`builtins` wrapper, filepath : `%s`;", args[0]) + context = get_current_context() + if not context: + return former_open(*args, **kwargs) + result = check_context_for_path_traversal( + filename=args[0], operation="builtins.open", context=context + ) + if len(result) != 0: + get_comms().send_data_to_bg_process("ATTACK", (result, context)) + should_block_res = get_comms().send_data_to_bg_process( + action="READ_PROPERTY", obj="block", receive=True + ) + if should_block_res["success"] and should_block_res["data"]: + raise AikidoPathTraversal() + return former_open(*args, **kwargs) + + # pylint: disable=no-member + setattr(builtins, "open", aikido_new_open) + logger.debug("Wrapped `builtins` module") + return modified_builtins diff --git a/aikido_firewall/sinks/os.py b/aikido_firewall/sinks/os.py new file mode 100644 index 000000000..a45aa10d2 --- /dev/null +++ b/aikido_firewall/sinks/os.py @@ -0,0 +1,96 @@ +""" +Sink module for python's `os` +""" + +import copy +import importhook +from aikido_firewall.helpers.logging import logger +from aikido_firewall.vulnerabilities.path_traversal.check_context_for_path_traversal import ( + check_context_for_path_traversal, +) +from aikido_firewall.context import get_current_context +from aikido_firewall.background_process import get_comms +from aikido_firewall.errors import AikidoPathTraversal + +# File functions : +OS_FILE_FUNCTIONS = [ + "access", + "chmod", + "chown", + "lstat", + "mkdir", + "listdir", + "readlink", + "unlink", + "rename", + "rmdir", + "remove", + "symlink", + "stat", + "utime", + "link", + "makedirs", + "walk", +] +OS_PATH_FUNCTIONS = [ + "exists", + "realpath", + "getsize", + "getmtime", + "getatime", + "getctime", +] +# os.path.join(path, *paths) is not wrapped + + +def generate_aikido_function(op, former_func): + """ + Returns a generated aikido function given an operation + and the previous function + """ + + def aikido_new_func(*args, op=op, former_func=former_func, **kwargs): + logger.debug("`os` wrapper, filepath : `%s`; OP : `%s`", args[0], op) + context = get_current_context() + if not context: + return former_func(*args, **kwargs) + result = check_context_for_path_traversal( + filename=args[0], operation=f"os.{op}", context=context + ) + if len(result) != 0: + get_comms().send_data_to_bg_process("ATTACK", (result, context)) + should_block_res = get_comms().send_data_to_bg_process( + action="READ_PROPERTY", obj="block", receive=True + ) + if should_block_res["success"] and should_block_res["data"]: + raise AikidoPathTraversal() + return former_func(*args, **kwargs) + + return aikido_new_func + + +@importhook.on_import("os") +def on_os_import(os): + """ + Hook 'n wrap on `os`, python's built-in functions + Our goal is to wrap the open() function, which you use when opening files + Returns : Modified os object + """ + modified_os = importhook.copy_module(os) + for op in OS_FILE_FUNCTIONS: + # Wrap os. functions + former_func = copy.deepcopy(getattr(os, op)) + aikido_new_func = generate_aikido_function(op, former_func) + setattr(os, op, aikido_new_func) + setattr(modified_os, op, aikido_new_func) + + for op in OS_PATH_FUNCTIONS: + # Wrap os.path functions + former_func = copy.deepcopy(getattr(os.path, op)) + aikido_new_func = generate_aikido_function(f"path.{op}", former_func) + setattr(os.path, op, aikido_new_func) + # pylint: disable=no-member + setattr(modified_os.path, op, aikido_new_func) + + logger.debug("Wrapped `os` module") + return modified_os diff --git a/sample-apps/flask-mysql/app.py b/sample-apps/flask-mysql/app.py index 76aa4237f..fc01eaafb 100644 --- a/sample-apps/flask-mysql/app.py +++ b/sample-apps/flask-mysql/app.py @@ -44,6 +44,17 @@ def create_dog(): connection.commit() return f'Dog {dog_name} created successfully' + +@app.route("/open_file", methods=['GET']) +def show_open_file_form(): + return render_template('open_file.html') + +@app.route("/open_file", methods=['POST']) +def open_file(): + filepath = request.form['filepath'] + file = open(filepath, 'r', encoding='utf-8') + return file.read() + @app.route("/request", methods=['GET']) def show_request_page(): return render_template('request.html') diff --git a/sample-apps/flask-mysql/templates/open_file.html b/sample-apps/flask-mysql/templates/open_file.html new file mode 100644 index 000000000..cd2eee8dd --- /dev/null +++ b/sample-apps/flask-mysql/templates/open_file.html @@ -0,0 +1,17 @@ + + + + + + + Open file + + +

Open file

+
+ + + +
+ +