diff --git a/FileFlow/backend/api/files.py b/FileFlow/backend/api/files.py index bb2750b5..e2f1cdb1 100644 --- a/FileFlow/backend/api/files.py +++ b/FileFlow/backend/api/files.py @@ -3,8 +3,10 @@ from werkzeug.utils import secure_filename try: from backend.models.database import db, File + from backend.utils.validators import Validators except ImportError: from models.database import db, File + from utils.validators import Validators from pathlib import Path files_bp = Blueprint('files_bp', __name__) @@ -111,11 +113,18 @@ def rename_file(file_id): new_name = data.get('new_name') if not new_name: - abort(400) - - file_to_rename.filename = new_name + abort(400, description="New name is required") + + # Validate and sanitize the filename + if not Validators.is_valid_filename(new_name): + abort(400, description="Invalid filename") + + # Sanitize the filename to remove any dangerous characters + sanitized_name = Validators.sanitize_filename(new_name) + + file_to_rename.filename = sanitized_name db.session.commit() - return jsonify({'success': True, 'new_name': new_name}) + return jsonify({'success': True, 'new_name': sanitized_name}) @files_bp.route('/move_file/', methods=['POST']) @login_required diff --git a/FileFlow/backend/app.py b/FileFlow/backend/app.py index 5f390db2..259fccf5 100644 --- a/FileFlow/backend/app.py +++ b/FileFlow/backend/app.py @@ -179,12 +179,6 @@ def open_folder(folder_id): app.logger.error(f"Error in open_folder: {str(e)}") abort(500, description="Error occurred while opening folder") -from datetime import datetime - -# ... (rest of the imports) - -# ... (rest of the code) - @app.route('/create_folder', methods=['POST']) @login_required def create_folder(): @@ -280,18 +274,21 @@ def init_db_command(): from backend.api.folders import folders_bp from backend.api.search import search_bp from backend.api.upload import upload_bp + from backend.api.compression import compression_bp except ImportError: from api.auth import auth_bp from api.files import files_bp from api.folders import folders_bp from api.search import search_bp from api.upload import upload_bp + from api.compression import compression_bp app.register_blueprint(files_bp) app.register_blueprint(folders_bp) app.register_blueprint(search_bp) app.register_blueprint(upload_bp) app.register_blueprint(auth_bp) +app.register_blueprint(compression_bp) import threading try: diff --git a/FileFlow/backend/config.py b/FileFlow/backend/config.py index 979edb19..dc04e2ab 100644 --- a/FileFlow/backend/config.py +++ b/FileFlow/backend/config.py @@ -1,8 +1,21 @@ import os +import warnings class Config: - SECRET_KEY = os.urandom(24) - SQLALCHEMY_DATABASE_URI = 'sqlite:///fileflow.db' + # SECRET_KEY should be set via environment variable in production + # A random fallback is used only for development; this will invalidate + # sessions on every restart + SECRET_KEY = os.environ.get('SECRET_KEY') + if not SECRET_KEY: + warnings.warn( + "SECRET_KEY not set in environment. Using random key. " + "Sessions will be invalidated on restart. " + "Set SECRET_KEY environment variable for production use.", + RuntimeWarning + ) + SECRET_KEY = os.urandom(24) + + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///fileflow.db' SQLALCHEMY_TRACK_MODIFICATIONS = False UPLOAD_FOLDER = 'user_files' MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file upload \ No newline at end of file diff --git a/FileFlow/backend/services/compression_service.py b/FileFlow/backend/services/compression_service.py index 7b1799ab..ae522a9a 100644 --- a/FileFlow/backend/services/compression_service.py +++ b/FileFlow/backend/services/compression_service.py @@ -4,6 +4,44 @@ from pathlib import Path class CompressionService: + @staticmethod + def _is_safe_path(base_path, target_path): + """Check if target path is within base path (prevents directory traversal) + + Note: This validates the path string without resolving symlinks to prevent + symlink-based attacks. The check uses string comparison after normalization. + """ + try: + base = Path(base_path).resolve() + # Normalize target without fully resolving to prevent symlink attacks + # Join base with target and check if it stays within base + full_path = (base / target_path).resolve() + return full_path.is_relative_to(base) + except (ValueError, RuntimeError): + return False + + @staticmethod + def _safe_extract_member(archive_path, member_name, extract_to): + """Validate that extracted file stays within extract_to directory + + Rejects paths with: + - Absolute paths + - Parent directory references (..) + - Paths that escape the extraction directory + """ + # Reject absolute paths + if Path(member_name).is_absolute(): + raise ValueError(f"Absolute path in archive is not allowed: {member_name}") + + # Reject paths with parent directory references + if '..' in Path(member_name).parts: + raise ValueError(f"Path traversal attempt detected: {member_name}") + + # Validate the final path stays within extraction directory + if not CompressionService._is_safe_path(extract_to, member_name): + raise ValueError(f"Attempted path traversal in archive: {member_name}") + return True + @staticmethod def create_zip(file_paths, archive_path, password=None): with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zipf: @@ -32,16 +70,30 @@ def extract_zip(archive_path, extract_to, password=None): with zipfile.ZipFile(archive_path, 'r') as zipf: if password: zipf.setpassword(password.encode()) + # Validate all members before extraction + for member in zipf.namelist(): + CompressionService._safe_extract_member(archive_path, member, extract_to) zipf.extractall(extract_to) @staticmethod def extract_tar(archive_path, extract_to): with tarfile.open(archive_path, 'r:*') as tarf: - tarf.extractall(extract_to) + # Validate all members before extraction to prevent path traversal + for member in tarf.getmembers(): + CompressionService._safe_extract_member(archive_path, member.name, extract_to) + # Use data filter for Python 3.11.4+ or validate manually for earlier versions + try: + tarf.extractall(extract_to, filter='data') + except TypeError: + # Python < 3.11.4 doesn't support filter parameter, but we already validated + tarf.extractall(extract_to) @staticmethod def extract_7z(archive_path, extract_to, password=None): with py7zr.SevenZipFile(archive_path, 'r', password=password) as szf: + # Validate all members before extraction + for member in szf.getnames(): + CompressionService._safe_extract_member(archive_path, member, extract_to) szf.extractall(extract_to) @staticmethod diff --git a/FileFlow/frontend/js/app.js/auth.js b/FileFlow/frontend/js/app.js/auth.js deleted file mode 100644 index e69de29b..00000000 diff --git a/FileFlow/frontend/js/app.js/file_management.js b/FileFlow/frontend/js/file_management.js similarity index 100% rename from FileFlow/frontend/js/app.js/file_management.js rename to FileFlow/frontend/js/file_management.js diff --git a/FileFlow/frontend/templates/dashboard.html b/FileFlow/frontend/templates/dashboard.html index 4ebfaac7..9bf08aa9 100644 --- a/FileFlow/frontend/templates/dashboard.html +++ b/FileFlow/frontend/templates/dashboard.html @@ -22,7 +22,7 @@
- +
@@ -64,15 +64,11 @@
- - - -