diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000..9e92f827 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,24 @@ +name: Benchmark +on: + push: {} + workflow_call: {} +jobs: + benchmark: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Run Docker Compose + working-directory: ./sample-apps/flask-mysql + run: | + docker compose -f docker-compose.yml -f docker-compose.benchmark.yml up --build -d + - name: Install K6 + uses: grafana/setup-k6-action@v1 + - name: Run flask-mysql k6 Benchmark + run: | + k6 run -q ./benchmarks/flask-mysql-benchmarks.js + diff --git a/aikido_firewall/background_process/__init__.py b/aikido_firewall/background_process/__init__.py index 2d14c287..48f26e3d 100644 --- a/aikido_firewall/background_process/__init__.py +++ b/aikido_firewall/background_process/__init__.py @@ -3,7 +3,9 @@ and listen for data sent by our sources and sinks """ +import os from aikido_firewall.helpers.token import get_token_from_env +from aikido_firewall.helpers.get_temp_dir import get_temp_dir from aikido_firewall.helpers.logging import logger from aikido_firewall.background_process.comms import ( AikidoIPCCommunications, @@ -11,7 +13,7 @@ reset_comms, ) -IPC_ADDRESS = ("localhost", 9898) # Specify the IP address and port +IPC_ADDRESS = get_temp_dir() + "/aikido_python_socket.sock" def start_background_process(): @@ -22,5 +24,9 @@ def start_background_process(): # Generate a secret key : secret_key_bytes = str.encode(str(get_token_from_env())) + # Remove the socket file if it already exists + if os.path.exists(IPC_ADDRESS): + os.remove(IPC_ADDRESS) + comms = AikidoIPCCommunications(IPC_ADDRESS, secret_key_bytes) comms.start_aikido_listener() diff --git a/aikido_firewall/helpers/get_temp_dir.py b/aikido_firewall/helpers/get_temp_dir.py new file mode 100644 index 00000000..0926d567 --- /dev/null +++ b/aikido_firewall/helpers/get_temp_dir.py @@ -0,0 +1,15 @@ +""" +Helper function file, see function docstring +""" + +import os + + +def get_temp_dir(): + """ + Checks the environment variable "AIKIDO_TMP_DIR" + """ + aikido_temp_dir_env = os.getenv("AIKIDO_TMP_DIR") + if aikido_temp_dir_env is not None: + return aikido_temp_dir_env + return "/tmp" diff --git a/aikido_firewall/sources/flask.py b/aikido_firewall/sources/flask.py index 1c78ecec..608d3543 100644 --- a/aikido_firewall/sources/flask.py +++ b/aikido_firewall/sources/flask.py @@ -10,34 +10,46 @@ from aikido_firewall.context import Context from aikido_firewall.background_process.packages import add_wrapped_package from .functions.request_handler import request_handler +from aikido_firewall.context import get_current_context -class AikidoMiddleware: +def generate_aikido_view_func_wrapper(former_view_func): """ - Aikido WSGI Middleware for ratelimiting and route reporting + Generates our own wrapper for the function in self.view_functions[] """ - def __init__(self, app, flask_app=None): - self.app = app - self.flask_app = flask_app - - def __call__(self, environ, start_response): - response = request_handler(stage="pre_response") - if response: - from flask import jsonify # We don't want to install flask - - with self.flask_app.app_context(): - start_response(f"{response[1]} Aikido", []) - return [response[0].encode("utf-8")] - - def custom_start_response(status, headers): - """Is current route useful snippet :""" - status_code = int(status.split(" ")[0]) + def aikido_view_func(*args, **kwargs): + from werkzeug.exceptions import HTTPException + from flask.globals import request_ctx + + req = request_ctx.request + # Set body : + context = get_current_context() + if context: + if req.is_json: + context.body = req.get_json() + context.set_as_current_context() + else: + context.body = req.form + context.set_as_current_context() + + pre_response = request_handler(stage="pre_response") + if pre_response: + return pre_response[0], pre_response[1] + try: + res = former_view_func(*args, **kwargs) + status_code = 200 + if isinstance(res, tuple): + status_code = res[1] + elif hasattr(res, "status_code"): + status_code = res.status_code request_handler(stage="post_response", status_code=status_code) - return start_response(status, headers) + return res + except HTTPException as e: + request_handler(stage="post_response", status_code=e.code) + raise e - response = self.app(environ, custom_start_response) - return response + return aikido_view_func def aikido___call__(flask_app, environ, start_response): @@ -46,13 +58,7 @@ def aikido___call__(flask_app, environ, start_response): # pylint: disable=import-outside-toplevel try: request_handler(stage="init") - # https://stackoverflow.com/a/11163649 : - length = int(environ.get("CONTENT_LENGTH") or 0) - body = environ["wsgi.input"].read(length) - # replace the stream since it was exhausted by read() - environ["wsgi.input"] = BytesIO(body) - - context1 = Context(req=environ, raw_body=body.decode("utf-8"), source="flask") + context1 = Context(req=environ, raw_body={}, source="flask") logger.debug("Context : %s", json.dumps(context1.__dict__)) context1.set_as_current_context() except Exception as e: @@ -67,18 +73,24 @@ def on_flask_import(flask): Hook 'n wrap on `flask.app` Our goal is to wrap the __init__ function of the "Flask" class, so we can insert our middleware. Returns : Modified flask.app object + + Flask class |-> App class |-> Scaffold class + @app.route is implemented in Scaffold and calls `add_url_rule` in App class + This function writes to self.view_functions[endpoint] = view_func + The only other reference where view_functions is called is on this line: + https://github.com/pallets/flask/blob/8a6cdf1e2a5efa81c30f6166602064ceefb0a35b/src/flask/app.py#L882 + So we would have to wrap the `ensure_sync` function of the app object """ modified_flask = importhook.copy_module(flask) - prev_flask_init = copy.deepcopy(flask.Flask.__init__) - - def aikido_flask_init(_self, *args, **kwargs): - prev_flask_init(_self, *args, **kwargs) - setattr(_self, "__call__", aikido___call__) - _self.wsgi_app = AikidoMiddleware(_self.wsgi_app, _self) + def aikido_ensure_sync(_self, func): + """ + We're wrapping this function, so we can wrap the passed along function `func` + https://github.com/pallets/flask/blob/8a6cdf1e2a5efa81c30f6166602064ceefb0a35b/src/flask/app.py#L946 + """ + return generate_aikido_view_func_wrapper(func) - # pylint: disable=no-member - setattr(modified_flask.Flask, "__init__", aikido_flask_init) setattr(modified_flask.Flask, "__call__", aikido___call__) + setattr(modified_flask.Flask, "ensure_sync", aikido_ensure_sync) add_wrapped_package("flask") return modified_flask diff --git a/benchmarks/flask-mysql-benchmarks.js b/benchmarks/flask-mysql-benchmarks.js new file mode 100644 index 00000000..926f3394 --- /dev/null +++ b/benchmarks/flask-mysql-benchmarks.js @@ -0,0 +1,130 @@ +import http from 'k6/http'; +import { check, sleep, fail } from 'k6'; +import exec from 'k6/execution'; +import { Trend } from 'k6/metrics'; + +const BASE_URL_8086 = 'http://localhost:8086'; +const BASE_URL_8087 = 'http://localhost:8087'; + +export const options = { + vus: 1, // Number of virtual users + thresholds: { + test_40mb_payload: [{ + threshold: "avg<2000", // Temporary exagurated threshold until optimizations are finished + abortOnFail: true, + delayAbortEval: '10s', + }], + test_multiple_queries: [{ + threshold: "avg<2000", + abortOnFail: true, + delayAbortEval: '10s', + }], + test_multiple_queries_with_big_body: [{ + threshold: "avg<2000", + abortOnFail: true, + delayAbortEval: '10s', + }], + test_create_with_big_body: [{ + threshold: "avg<2000", + abortOnFail: true, + delayAbortEval: '10s', + }], + test_normal_route: [{ + threshold: "avg<2000", + abortOnFail: true, + delayAbortEval: '10s', + }], + test_id_route: [{ + threshold: "avg<2000", + abortOnFail: true, + delayAbortEval: '10s', + }], + test_open_file: [{ + threshold: "avg<2000", + abortOnFail: true, + delayAbortEval: '10s', + }], + test_execute_shell: [{ + threshold: "avg<2000", + abortOnFail: true, + delayAbortEval: '10s', + }], + + }, +}; +const default_headers = { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", +}; + +const default_payload = { + dog_name: "Pops", + other_dogs: Array(2000).fill("Lorem Ipsum"), + other_dogs2: Array.from({length: 5000}, () => Math.floor(Math.random() * 99999999)), + text_message: "Lorem ipsum dolor sit amet".repeat(3000) + +}; +function generateLargeJson(sizeInMB) { + const sizeInBytes = sizeInMB * 1024; // Convert MB to Kilobytes + let long_text = "b".repeat(sizeInBytes) + return { + dog_name: "test", + long_texts: new Array(1024).fill(long_text) + } +} + +function measureRequest(url, method = 'GET', payload, status_code=200, headers=default_headers) { + let res; + if (method === 'POST') { + res = http.post(url, payload, { + headers: headers + } + ); + } else { + res = http.get(url, { + headers: headers + }); + } + check(res, { + 'status is correct': (r) => r.status === status_code, + }); + return res.timings.duration; // Return the duration of the request +} + +function route_test(trend, amount, route, method="GET", data=default_payload, status=200) { + for (let i = 0; i < amount; i++) { + let time_with_fw = measureRequest(BASE_URL_8086 + route, method, data, status) + let time_without_fw = measureRequest(BASE_URL_8087 + route, method, data, status) + trend.add(time_with_fw - time_without_fw) + } +} + +export function handleSummary(data) { + for (const [metricName, metricValue] of Object.entries(data.metrics)) { + if(!metricName.startsWith('test_') || metricValue.values.avg == 0) { + continue + } + let values = metricValue.values + console.log(`\x1b[35m 🚅 ${metricName}\x1b[0m: ΔAverage is \x1b[4m${values.avg.toFixed(2)}ms\x1b[0m | ΔMedian is \x1b[4m${values.med.toFixed(2)}ms\x1b[0m`); + } + return {stdout: ""}; +} + +let test_40mb_payload = new Trend('test_40mb_payload') +let test_multiple_queries = new Trend("test_multiple_queries") +let test_multiple_queries_with_big_body = new Trend("test_multiple_queries_with_big_body") +let test_create_with_big_body = new Trend("test_create_with_big_body") +let test_normal_route = new Trend("test_normal_route") +let test_id_route = new Trend("test_id_route") +let test_open_file = new Trend("test_open_file") +let test_execute_shell = new Trend("test_execute_shell") +export default function () { + route_test(test_40mb_payload, 30, "/create", "POST", generateLargeJson(40)) // 40 Megabytes + route_test(test_multiple_queries, 50, "/multiple_queries", "POST", {dog_name: "W"}) + route_test(test_multiple_queries_with_big_body, 50, "/multiple_queries", "POST") + route_test(test_create_with_big_body, 500, "/create", "POST") + route_test(test_normal_route, 500, "/") + route_test(test_id_route, 500, "/dogpage/1") + route_test(test_open_file, 500, "/open_file", 'POST', { filepath: '.env.example' }) + route_test(test_execute_shell, 500, "/shell", "POST", { command: 'xyzwh'}) +} diff --git a/sample-apps/README.md b/sample-apps/README.md index af10ec8f..c7f28143 100644 --- a/sample-apps/README.md +++ b/sample-apps/README.md @@ -1,14 +1,20 @@ # Aikido's sample apps Overview : -- `django-mysql/` is a Django app using MySQL. +- `django-mysql/` is a Django app using MySQL. - It runs **multi-threaded** + - Runs on 8080. Without Aikido runs on 8081 - `django-mysql-gunicorn/` is a Django app using MySQL and runnin with a Gunicorn backend. - it runs 4 processes, called workers, (**multi-process**) which handle requests using 2 threads (**multi-threaded**) + - Runs on 8082. Without Aikido runs on 8083 - `flask-mongo/` is a Flask app using MongoDB. - It runs **multi-threaded** + - Runs on 8084. Without Aikido runs on 8085 - `flask-mysql/` is a Flask app using MySQL. - It runs **single-threaded** + - Runs on 8086. Without Aikido runs on 8087 - `flask-mysql-uwsgi/` is a Flask app using Mysql and running with a uWSGI backend. - It runs 4 processes (**multi-process**) which handle requests **multi-threaded** + - Runs on 8088. Without aikido runs on 8089 - `flask-postres/` is a Flask app using Postgres - It runs **multi-threaded** + - Runs on 8090. Without aikido runs on 8091 diff --git a/sample-apps/django-mysql-gunicorn/.env.example b/sample-apps/django-mysql-gunicorn/.env.example index e5056a7c..88082352 100644 --- a/sample-apps/django-mysql-gunicorn/.env.example +++ b/sample-apps/django-mysql-gunicorn/.env.example @@ -7,3 +7,4 @@ MYSQL_ROOT_PASSWORD="password" # Aikido keys AIKIDO_DEBUG=true AIKIDO_TOKEN="AIK_secret_token" +AIKIDO_BLOCKING=true diff --git a/sample-apps/django-mysql-gunicorn/README.md b/sample-apps/django-mysql-gunicorn/README.md index 8577f666..1792d354 100644 --- a/sample-apps/django-mysql-gunicorn/README.md +++ b/sample-apps/django-mysql-gunicorn/README.md @@ -6,11 +6,11 @@ With docker-compose installed run ```bash docker-compose up --build ``` -This will expose a Django web server at [localhost:8080](http://localhost:8080) +This will expose a Django web server at [localhost:8082](http://localhost:8082) ## URLS : -- Homepage : `http://localhost:8080/app` -- Create a dog : `http://localhost:8080/app/create/` +- Homepage : `http://localhost:8082/app` +- Create a dog : `http://localhost:8082/app/create/` - MySQL attack : Enter `Malicious dog", "Injected wrong boss name"); -- ` To verify your attack was successfully note that the boss_name usualy is 'N/A', if you open the dog page (you can do this from the home page). You should see a "malicious dog" with a boss name that is not permitted. diff --git a/sample-apps/django-mysql-gunicorn/docker-compose.benchmark.yml b/sample-apps/django-mysql-gunicorn/docker-compose.benchmark.yml new file mode 100644 index 00000000..26d1e720 --- /dev/null +++ b/sample-apps/django-mysql-gunicorn/docker-compose.benchmark.yml @@ -0,0 +1,24 @@ +version: "3" +services: + backend_firewall_disabled: + build: + context: ./../../ + dockerfile: ./sample-apps/django-mysql-gunicorn/Dockerfile + command: sh -c "python manage.py migrate && gunicorn -c gunicorn_config.py --workers 4 --threads 2 --log-level debug --access-logfile '-' --error-logfile '-' --bind 0.0.0.0:8000 sample-django-mysql-gunicorn-app.wsgi" + restart: always + volumes: + - .:/app + ports: + - "8083:8000" + env_file: + - .env + depends_on: + db: + condition: service_healthy + extra_hosts: + - "app.local.aikido.io:host-gateway" + environment: + - FIREWALL_DISABLED=1 + backend: + environment: + - AIKIDO_TOKEN="test_aikido_token" diff --git a/sample-apps/django-mysql-gunicorn/docker-compose.yml b/sample-apps/django-mysql-gunicorn/docker-compose.yml index 4b16391f..e0099209 100644 --- a/sample-apps/django-mysql-gunicorn/docker-compose.yml +++ b/sample-apps/django-mysql-gunicorn/docker-compose.yml @@ -11,12 +11,6 @@ services: MYSQL_USER: 'user' MYSQL_PASSWORD: 'password' MYSQL_ROOT_PASSWORD: 'password' - ports: - # : - - '3306:3306' - expose: - # Opens port 3306 on the container - - '3306' healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 5s @@ -33,7 +27,7 @@ services: volumes: - .:/app ports: - - "8080:8000" + - "8082:8000" env_file: - .env depends_on: diff --git a/sample-apps/django-mysql-gunicorn/gunicorn_config.py b/sample-apps/django-mysql-gunicorn/gunicorn_config.py index 396a74be..b30bcbbb 100644 --- a/sample-apps/django-mysql-gunicorn/gunicorn_config.py +++ b/sample-apps/django-mysql-gunicorn/gunicorn_config.py @@ -1,5 +1,11 @@ -import aikido_firewall.middleware.django_gunicorn as aik -@aik.when_ready -def when_ready(server): pass -@aik.post_fork -def post_fork(server, worker): pass +from dotenv import load_dotenv +import os +load_dotenv() +firewall_disabled = os.getenv("FIREWALL_DISABLED") +if firewall_disabled is not None: + if firewall_disabled.lower() != "1": + import aikido_firewall.middleware.django_gunicorn as aik + @aik.when_ready + def when_ready(server): pass + @aik.post_fork + def post_fork(server, worker): pass diff --git a/sample-apps/django-mysql/.env.example b/sample-apps/django-mysql/.env.example index e5056a7c..88082352 100644 --- a/sample-apps/django-mysql/.env.example +++ b/sample-apps/django-mysql/.env.example @@ -7,3 +7,4 @@ MYSQL_ROOT_PASSWORD="password" # Aikido keys AIKIDO_DEBUG=true AIKIDO_TOKEN="AIK_secret_token" +AIKIDO_BLOCKING=true diff --git a/sample-apps/django-mysql/docker-compose.benchmark.yml b/sample-apps/django-mysql/docker-compose.benchmark.yml new file mode 100644 index 00000000..92e5086f --- /dev/null +++ b/sample-apps/django-mysql/docker-compose.benchmark.yml @@ -0,0 +1,23 @@ +version: "3" +services: + backend_firewall_disabled: + build: + context: ./../../ + dockerfile: ./sample-apps/django-mysql/Dockerfile + command: sh -c "python3 manage.py migrate --noinput && python manage.py runserver 0.0.0.0:8000" + restart: always + volumes: + - .:/app + ports: + - "8081:8000" + env_file: + - .env + depends_on: + - db + extra_hosts: + - "app.local.aikido.io:host-gateway" + environment: + - FIREWALL_DISABLED=1 + backend: + environment: + - AIKIDO_TOKEN="test_aikido_token" diff --git a/sample-apps/django-mysql/docker-compose.yml b/sample-apps/django-mysql/docker-compose.yml index ffdbfa6a..b1846b82 100644 --- a/sample-apps/django-mysql/docker-compose.yml +++ b/sample-apps/django-mysql/docker-compose.yml @@ -11,12 +11,6 @@ services: MYSQL_USER: 'user' MYSQL_PASSWORD: 'password' MYSQL_ROOT_PASSWORD: 'password' - ports: - # : - - '3306:3306' - expose: - # Opens port 3306 on the container - - '3306' backend: build: diff --git a/sample-apps/django-mysql/manage.py b/sample-apps/django-mysql/manage.py index 15437c74..4499f0c6 100755 --- a/sample-apps/django-mysql/manage.py +++ b/sample-apps/django-mysql/manage.py @@ -1,13 +1,20 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" +from dotenv import load_dotenv +import os +load_dotenv() +firewall_disabled = os.getenv("FIREWALL_DISABLED") +if firewall_disabled is not None: + if firewall_disabled.lower() != "1": + import aikido_firewall # Aikido package import + aikido_firewall.protect() + import os import sys -import aikido_firewall # Aikido module def main(): """Run administrative tasks.""" - aikido_firewall.protect("django") os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample-django-mysql-app.settings') try: from django.core.management import execute_from_command_line diff --git a/sample-apps/flask-mongo/.env.example b/sample-apps/flask-mongo/.env.example index 07c58d41..f04b5012 100644 --- a/sample-apps/flask-mongo/.env.example +++ b/sample-apps/flask-mongo/.env.example @@ -1,2 +1,3 @@ AIKIDO_DEBUG=true AIKIDO_TOKEN="AIK_secret_token" +AIKIDO_BLOCKING=true diff --git a/sample-apps/flask-mongo/README.md b/sample-apps/flask-mongo/README.md index 6c95edcb..017851dd 100644 --- a/sample-apps/flask-mongo/README.md +++ b/sample-apps/flask-mongo/README.md @@ -7,6 +7,6 @@ Run (with docker-compose installed) : 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/` -- To test the nosql injection go to `http://localhost:8080/auth/` +- You'll be able to access the Flask Server at : [localhost:8084](http://localhost:8084) +- To Create a reference test dog use `http://localhost:8084/create/` +- To test the nosql injection go to `http://localhost:8084/auth/` diff --git a/sample-apps/flask-mongo/app.py b/sample-apps/flask-mongo/app.py index 121f7f11..f89d2d06 100644 --- a/sample-apps/flask-mongo/app.py +++ b/sample-apps/flask-mongo/app.py @@ -1,5 +1,11 @@ -import aikido_firewall # Aikido package import -aikido_firewall.protect() +from dotenv import load_dotenv +import os +load_dotenv() +firewall_disabled = os.getenv("FIREWALL_DISABLED") +if firewall_disabled is not None: + if firewall_disabled.lower() != "1": + import aikido_firewall # Aikido package import + aikido_firewall.protect() import json from flask import Flask, render_template, request diff --git a/sample-apps/flask-mongo/docker-compose.benchmark.yml b/sample-apps/flask-mongo/docker-compose.benchmark.yml new file mode 100644 index 00000000..ad289ae0 --- /dev/null +++ b/sample-apps/flask-mongo/docker-compose.benchmark.yml @@ -0,0 +1,23 @@ +version: "3" +services: + backend_firewall_disabled: + build: + context: ./../../ + dockerfile: ./sample-apps/flask-mongo/Dockerfile + command: sh -c "flask --app app run --debug --host=0.0.0.0" + restart: always + volumes: + - .:/app + ports: + - "8085:5000" + depends_on: + - db + networks: + - default_network + extra_hosts: + - "app.local.aikido.io:host-gateway" + environment: + - FIREWALL_DISABLED=1 + backend: + environment: + - AIKIDO_TOKEN="test_aikido_token" diff --git a/sample-apps/flask-mongo/docker-compose.yml b/sample-apps/flask-mongo/docker-compose.yml index d8186013..e42feb1a 100644 --- a/sample-apps/flask-mongo/docker-compose.yml +++ b/sample-apps/flask-mongo/docker-compose.yml @@ -26,7 +26,7 @@ services: volumes: - .:/app ports: - - "8080:5000" + - "8084:5000" depends_on: - db networks: diff --git a/sample-apps/flask-mysql-uwsgi/.env.example b/sample-apps/flask-mysql-uwsgi/.env.example index 07c58d41..f04b5012 100644 --- a/sample-apps/flask-mysql-uwsgi/.env.example +++ b/sample-apps/flask-mysql-uwsgi/.env.example @@ -1,2 +1,3 @@ AIKIDO_DEBUG=true AIKIDO_TOKEN="AIK_secret_token" +AIKIDO_BLOCKING=true diff --git a/sample-apps/flask-mysql-uwsgi/README.md b/sample-apps/flask-mysql-uwsgi/README.md index 88ed327f..5e218e80 100644 --- a/sample-apps/flask-mysql-uwsgi/README.md +++ b/sample-apps/flask-mysql-uwsgi/README.md @@ -7,6 +7,6 @@ Run (with docker-compose installed) : 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` +- You'll be able to access the Flask Server at : [localhost:8088](http://localhost:8088) +- To Create a reference test dog use `http://localhost:8088/create/doggo` - To test a sql injection enter the following dog name : `Malicious dog", 1); -- ` diff --git a/sample-apps/flask-mysql-uwsgi/app.py b/sample-apps/flask-mysql-uwsgi/app.py index b10c8541..039ce02a 100644 --- a/sample-apps/flask-mysql-uwsgi/app.py +++ b/sample-apps/flask-mysql-uwsgi/app.py @@ -1,5 +1,11 @@ -import aikido_firewall # Aikido package import -aikido_firewall.protect() +from dotenv import load_dotenv +import os +load_dotenv() +firewall_disabled = os.getenv("FIREWALL_DISABLED") +if firewall_disabled is not None: + if firewall_disabled.lower() != "1": + import aikido_firewall # Aikido package import + aikido_firewall.protect() from flask import Flask, render_template, request from flaskext.mysql import MySQL diff --git a/sample-apps/flask-mysql-uwsgi/docker-compose.benchmark.yml b/sample-apps/flask-mysql-uwsgi/docker-compose.benchmark.yml new file mode 100644 index 00000000..49f66bb0 --- /dev/null +++ b/sample-apps/flask-mysql-uwsgi/docker-compose.benchmark.yml @@ -0,0 +1,21 @@ +version: "3" +services: + backend_firewall_disabled: + build: + context: ./../../ + dockerfile: ./sample-apps/flask-mysql-uwsgi/Dockerfile + command: sh -c "uwsgi --ini uwsgi.ini" + restart: always + volumes: + - .:/app + ports: + - "8089:5000" + depends_on: + - db + extra_hosts: + - "app.local.aikido.io:host-gateway" + environment: + - FIREWALL_DISABLED=1 + backend: + environment: + - AIKIDO_TOKEN="test_aikido_token"" diff --git a/sample-apps/flask-mysql-uwsgi/docker-compose.yml b/sample-apps/flask-mysql-uwsgi/docker-compose.yml index cbd98ed0..166268eb 100644 --- a/sample-apps/flask-mysql-uwsgi/docker-compose.yml +++ b/sample-apps/flask-mysql-uwsgi/docker-compose.yml @@ -13,12 +13,7 @@ services: MYSQL_PASSWORD: 'password' MYSQL_ROOT_PASSWORD: 'password' command: --init-file /data/application/init.sql - ports: - # : - - '3306:3306' - expose: - # Opens port 3306 on the container - - '3306' + backend: build: @@ -30,11 +25,13 @@ services: volumes: - .:/app ports: - - "8080:5000" + - "8088:5000" depends_on: - db extra_hosts: - "app.local.aikido.io:host-gateway" + environment: + - FIREWALL_DISABLED=0 volumes: diff --git a/sample-apps/flask-mysql/.env.example b/sample-apps/flask-mysql/.env.example index 07c58d41..f04b5012 100644 --- a/sample-apps/flask-mysql/.env.example +++ b/sample-apps/flask-mysql/.env.example @@ -1,2 +1,3 @@ AIKIDO_DEBUG=true AIKIDO_TOKEN="AIK_secret_token" +AIKIDO_BLOCKING=true diff --git a/sample-apps/flask-mysql/README.md b/sample-apps/flask-mysql/README.md index 24f3e2d5..0f3e1dde 100644 --- a/sample-apps/flask-mysql/README.md +++ b/sample-apps/flask-mysql/README.md @@ -7,7 +7,7 @@ Run (with docker-compose installed) : 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` +- You'll be able to access the Flask Server at : [localhost:8086](http://localhost:8086) +- To Create a reference test dog use `http://localhost:8086/create/doggo` - To test a sql injection enter the following dog name : `Malicious dog", 1); -- ` -- To test commands : `http://localhost:8080/shell`, uses subprocess.run +- To test commands : `http://localhost:8086/shell`, uses subprocess.run diff --git a/sample-apps/flask-mysql/app.py b/sample-apps/flask-mysql/app.py index 15cb1f3e..ed9b5907 100644 --- a/sample-apps/flask-mysql/app.py +++ b/sample-apps/flask-mysql/app.py @@ -1,5 +1,9 @@ -import aikido_firewall # Aikido package import -aikido_firewall.protect() +import os +firewall_disabled = os.getenv("FIREWALL_DISABLED") +if firewall_disabled is not None: + if firewall_disabled.lower() != "1": + import aikido_firewall # Aikido package import + aikido_firewall.protect() import subprocess from flask import Flask, render_template, request @@ -20,10 +24,6 @@ @app.route("/") def homepage(): - aikido_firewall.set_user({ - "id": 1, - "name": "Wout" - }) cursor = mysql.get_db().cursor() cursor.execute("SELECT * FROM db.dogs") dogs = cursor.fetchall() @@ -32,10 +32,6 @@ def homepage(): @app.route('/dogpage/') def get_dogpage(dog_id): - aikido_firewall.set_user({ - "id": 2, - "name": "Wout 2" - }) cursor = mysql.get_db().cursor() cursor.execute("SELECT * FROM db.dogs WHERE id = " + str(dog_id)) dog = cursor.fetchmany(1)[0] @@ -45,6 +41,15 @@ def get_dogpage(dog_id): def show_create_dog_form(): return render_template('create_dog.html') +@app.route("/multiple_queries", methods=['POST']) +def multiple_queries(): + dog_name = request.form['dog_name'] + cursor = mysql.get_db().cursor() + for i in range(20): + cursor.execute(f'SELECT * FROM db.dogs WHERE dog_name = "%s"' % (dog_name)) + cursor.fetchmany(1) + return f'OK' + @app.route("/create", methods=['POST']) def create_dog(): dog_name = request.form['dog_name'] @@ -82,13 +87,3 @@ def make_request(): url = request.form['url'] res = requests.get(url) return str(res) - -@app.route("/execute", methods=['GET']) -def show_execute_page(): - return render_template('execute.html') - -@app.route("/execute", methods=['POST']) -def run_command(): - command = request.form['command'].split() - result = subprocess.run(command, capture_output=True, text=True) - return str(result.stdout) diff --git a/sample-apps/flask-mysql/docker-compose.benchmark.yml b/sample-apps/flask-mysql/docker-compose.benchmark.yml new file mode 100644 index 00000000..17a14f26 --- /dev/null +++ b/sample-apps/flask-mysql/docker-compose.benchmark.yml @@ -0,0 +1,21 @@ +version: "3" +services: + backend_firewall_disabled: + build: + context: ./../../ + dockerfile: ./sample-apps/flask-mysql/Dockerfile + command: sh -c "flask --app app run --debug --host=0.0.0.0 --no-reload" + restart: always + volumes: + - .:/app + ports: + - "8087:5000" + depends_on: + - db + extra_hosts: + - "app.local.aikido.io:host-gateway" + environment: + - FIREWALL_DISABLED=1 + backend: + environment: + - AIKIDO_TOKEN="test_aikido_token" diff --git a/sample-apps/flask-mysql/docker-compose.yml b/sample-apps/flask-mysql/docker-compose.yml index 86f34326..3b243cd3 100644 --- a/sample-apps/flask-mysql/docker-compose.yml +++ b/sample-apps/flask-mysql/docker-compose.yml @@ -13,12 +13,6 @@ services: MYSQL_PASSWORD: 'password' MYSQL_ROOT_PASSWORD: 'password' command: --init-file /data/application/init.sql - ports: - # : - - '3306:3306' - expose: - # Opens port 3306 on the container - - '3306' backend: build: @@ -30,11 +24,13 @@ services: volumes: - .:/app ports: - - "8080:5000" + - "8086:5000" depends_on: - db extra_hosts: - "app.local.aikido.io:host-gateway" + environment: + - FIREWALL_DISABLED=0 volumes: db_data: diff --git a/sample-apps/flask-postgres/.env.example b/sample-apps/flask-postgres/.env.example index 07c58d41..f04b5012 100644 --- a/sample-apps/flask-postgres/.env.example +++ b/sample-apps/flask-postgres/.env.example @@ -1,2 +1,3 @@ AIKIDO_DEBUG=true AIKIDO_TOKEN="AIK_secret_token" +AIKIDO_BLOCKING=true diff --git a/sample-apps/flask-postgres/README.md b/sample-apps/flask-postgres/README.md index 2c1f07df..8c91dda5 100644 --- a/sample-apps/flask-postgres/README.md +++ b/sample-apps/flask-postgres/README.md @@ -7,8 +7,8 @@ Run (with docker-compose installed) : 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/` -- To Create a reference test dog (with executemany) use `http://localhost:8080/create_many/` +- You'll be able to access the Flask Server at : [localhost:8090](http://localhost:8090) +- To Create a reference test dog use `http://localhost:8090/create/` +- To Create a reference test dog (with executemany) use `http://localhost:8090/create_many/` - To test a sql injection enter the following dog name : `Malicious dog', TRUE); -- ` diff --git a/sample-apps/flask-postgres/app.py b/sample-apps/flask-postgres/app.py index 16bc14ee..f17c436c 100644 --- a/sample-apps/flask-postgres/app.py +++ b/sample-apps/flask-postgres/app.py @@ -1,5 +1,11 @@ -import aikido_firewall # Aikido package import -aikido_firewall.protect() +from dotenv import load_dotenv +import os +load_dotenv() +firewall_disabled = os.getenv("FIREWALL_DISABLED") +if firewall_disabled is not None: + if firewall_disabled.lower() != "1": + import aikido_firewall # Aikido package import + aikido_firewall.protect() from flask import Flask, render_template, request import psycopg2 diff --git a/sample-apps/flask-postgres/docker-compose.benchmark.yml b/sample-apps/flask-postgres/docker-compose.benchmark.yml new file mode 100644 index 00000000..517e6739 --- /dev/null +++ b/sample-apps/flask-postgres/docker-compose.benchmark.yml @@ -0,0 +1,23 @@ +version: "3" +services: + backend_firewall_disabled: + build: + context: ./../../ + dockerfile: ./sample-apps/flask-postgres/Dockerfile + command: sh -c "flask --app app run --debug --host=0.0.0.0" + restart: always + volumes: + - .:/app + ports: + - "8091:5000" + depends_on: + - db + networks: + - default_network + extra_hosts: + - "app.local.aikido.io:host-gateway" + environment: + - FIREWALL_DISABLED=1 + backend: + environment: + - AIKIDO_TOKEN="test_aikido_token" diff --git a/sample-apps/flask-postgres/docker-compose.yml b/sample-apps/flask-postgres/docker-compose.yml index 7ddb8545..50b16235 100644 --- a/sample-apps/flask-postgres/docker-compose.yml +++ b/sample-apps/flask-postgres/docker-compose.yml @@ -11,10 +11,6 @@ services: POSTGRES_DB: 'db' POSTGRES_USER: 'user' POSTGRES_PASSWORD: 'password' - ports: - - '5432:5432' - expose: - - '5432' networks: - default_network @@ -28,14 +24,15 @@ services: volumes: - .:/app ports: - - "8080:5000" + - "8090:5000" depends_on: - db networks: - default_network extra_hosts: - "app.local.aikido.io:host-gateway" - + environment: + - FIREWALL_DISABLED=0 volumes: db_data2: