From 055c8b58b1fac0ef098704dc804e90dfbc721efe Mon Sep 17 00:00:00 2001 From: Aarav Anand Date: Fri, 6 Mar 2026 03:53:24 +0530 Subject: [PATCH 1/2] Fix #40: Enforce request size limits to prevent unbounded file uploads --- API/Routes/Upload/UploadRoute.py | 47 +++++++++++++++++++++++++++++++- API/app.py | 9 +++++- README.md | 6 ++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/API/Routes/Upload/UploadRoute.py b/API/Routes/Upload/UploadRoute.py index 88dde7d6a..54a1ec785 100644 --- a/API/Routes/Upload/UploadRoute.py +++ b/API/Routes/Upload/UploadRoute.py @@ -1,5 +1,5 @@ import shutil -from flask import Blueprint, request, jsonify, send_file, after_this_request +from flask import Blueprint, request, jsonify, send_file, after_this_request, current_app from zipfile import ZipFile from pathlib import Path from werkzeug.utils import secure_filename @@ -256,10 +256,24 @@ def uploadCaseUnchunked_old(): if submitted_file and allowed_filename(submitted_file): filename = secure_filename(submitted_file) + + max_len = current_app.config.get("MAX_CONTENT_LENGTH") + if max_len: + file.seek(0, os.SEEK_END) + file_length = file.tell() + file.seek(0) + if file_length > max_len: + return jsonify({"error": "File exceeds maximum allowed size"}), 413 + #spasiti zip u data storage file.save(os.path.join(Config.DATA_STORAGE, filename)) #zipfiles = [] with ZipFile(os.path.join(Config.DATA_STORAGE, filename)) as zf: + MAX_ZIP_UNCOMPRESSED = 1 * 1024 * 1024 * 1024 # 1GB + if sum(info.file_size for info in zf.infolist()) > MAX_ZIP_UNCOMPRESSED: + os.remove(os.path.join(Config.DATA_STORAGE, filename)) + return jsonify({"error": "File exceeds maximum allowed size"}), 413 + errorcode = 1 for zippedfile in zf.namelist(): # one = zippedfile @@ -424,6 +438,11 @@ def handle_full_zip(file, filepath=None): filename = secure_filename(submitted_file) with ZipFile(filepath) as zf: + MAX_ZIP_UNCOMPRESSED = 1 * 1024 * 1024 * 1024 # 1GB + if sum(info.file_size for info in zf.infolist()) > MAX_ZIP_UNCOMPRESSED: + os.remove(filepath) + return jsonify({"error": "File exceeds maximum allowed size"}), 413 + errorcode = 1 @@ -553,6 +572,13 @@ def uploadCase(): # Ako nije chunked upload (chrome browser dev mode) if dz_uuid is None: + max_len = current_app.config.get("MAX_CONTENT_LENGTH") + if max_len: + file.seek(0, os.SEEK_END) + file_length = file.tell() + file.seek(0) + if file_length > max_len: + return jsonify({"error": "File exceeds maximum allowed size"}), 413 # ========================== # TVOJ ORIGINALNI KOD # ========================== @@ -568,6 +594,16 @@ def uploadCase(): chunk_dir = os.path.join(Config.DATA_STORAGE, "_chunks", dz_uuid) os.makedirs(chunk_dir, exist_ok=True) + max_len = current_app.config.get("MAX_CONTENT_LENGTH") + if max_len: + current_size = sum(os.path.getsize(os.path.join(chunk_dir, f)) for f in os.listdir(chunk_dir) if os.path.isfile(os.path.join(chunk_dir, f))) + file.seek(0, os.SEEK_END) + chunk_size = file.tell() + file.seek(0) + if current_size + chunk_size > max_len: + shutil.rmtree(chunk_dir) + return jsonify({"error": "File exceeds maximum allowed size"}), 413 + chunk_path = os.path.join(chunk_dir, f"chunk_{dz_chunk_index}") file.save(chunk_path) @@ -620,6 +656,15 @@ def uploadXls(): if submitted_file and allowed_filename_xls(submitted_file): filename = secure_filename(submitted_file) + + max_len = current_app.config.get("MAX_CONTENT_LENGTH") + if max_len: + file.seek(0, os.SEEK_END) + file_length = file.tell() + file.seek(0) + if file_length > max_len: + return jsonify({"error": "File exceeds maximum allowed size"}), 413 + #spasiti zip u data storage file.save(os.path.join(Config.DATA_STORAGE, filename)) diff --git a/API/app.py b/API/app.py index f2fc8c476..3e1e40472 100644 --- a/API/app.py +++ b/API/app.py @@ -43,7 +43,8 @@ app.permanent_session_lifetime = timedelta(days=5) app.config['SECRET_KEY'] = '12345' -app.config["MAX_CONTENT_LENGTH"] = None +UPLOAD_MAX_SIZE = int(os.getenv("UPLOAD_MAX_SIZE", 500 * 1024 * 1024)) +app.config["MAX_CONTENT_LENGTH"] = UPLOAD_MAX_SIZE app.register_blueprint(upload_api) app.register_blueprint(case_api) @@ -73,6 +74,12 @@ def add_headers(response): # response.status_code = error.status_code # return response +@app.errorhandler(413) +def request_entity_too_large(error): + return jsonify({ + "error": "Uploaded file is too large" + }), 413 + #entry point to frontend @app.route("/", methods=['GET']) def home(): diff --git a/README.md b/README.md index 001bb8b91..24586b0a1 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ This repository contains the user interface for the Open Source Energy Modelling 4. The App will open automatically once the installation is complete. If not, search on the Windows Taskbar for ‘’MUIO’’ and open the App. 5. You will see the MUIO in a new window. +## Environment Configuration + +- `UPLOAD_MAX_SIZE` + Example: `UPLOAD_MAX_SIZE=524288000` + This overrides the default request size limit (500 MB) to prevent unbounded file uploads. + ## Questions and Issues For troubleshooting model-related issues and discussions, please visit the [Energy Modelling Community Discussion Forum](https://forum.u4ria.org/). From 2f92c0c67d837082943835456c7012a0908ba88c Mon Sep 17 00:00:00 2001 From: Aarav Anand Date: Fri, 6 Mar 2026 04:20:49 +0530 Subject: [PATCH 2/2] Fix file locks when deleting oversized zip files --- API/Routes/Upload/UploadRoute.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/API/Routes/Upload/UploadRoute.py b/API/Routes/Upload/UploadRoute.py index 54a1ec785..dcdeb8fdf 100644 --- a/API/Routes/Upload/UploadRoute.py +++ b/API/Routes/Upload/UploadRoute.py @@ -271,9 +271,15 @@ def uploadCaseUnchunked_old(): with ZipFile(os.path.join(Config.DATA_STORAGE, filename)) as zf: MAX_ZIP_UNCOMPRESSED = 1 * 1024 * 1024 * 1024 # 1GB if sum(info.file_size for info in zf.infolist()) > MAX_ZIP_UNCOMPRESSED: - os.remove(os.path.join(Config.DATA_STORAGE, filename)) - return jsonify({"error": "File exceeds maximum allowed size"}), 413 + too_large = True + else: + too_large = False + + if too_large: + os.remove(os.path.join(Config.DATA_STORAGE, filename)) + return jsonify({"error": "File exceeds maximum allowed size"}), 413 + with ZipFile(os.path.join(Config.DATA_STORAGE, filename)) as zf: errorcode = 1 for zippedfile in zf.namelist(): # one = zippedfile @@ -440,9 +446,15 @@ def handle_full_zip(file, filepath=None): with ZipFile(filepath) as zf: MAX_ZIP_UNCOMPRESSED = 1 * 1024 * 1024 * 1024 # 1GB if sum(info.file_size for info in zf.infolist()) > MAX_ZIP_UNCOMPRESSED: - os.remove(filepath) - return jsonify({"error": "File exceeds maximum allowed size"}), 413 + too_large = True + else: + too_large = False + + if too_large: + os.remove(filepath) + return jsonify({"error": "File exceeds maximum allowed size"}), 413 + with ZipFile(filepath) as zf: errorcode = 1