Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions server/mergin/sync/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,5 @@ class Configuration(object):
EXCLUDED_CLONE_FILENAMES = config(
"EXCLUDED_CLONE_FILENAMES", default="qgis_cfg.xml", cast=Csv()
)
# files that should be ignored during extension and mime type check
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment grammar/casing: consider using “MIME” and pluralizing “checks” (e.g., “ignored during extension and MIME type checks”) for clarity and consistency.

Suggested change
# files that should be ignored during extension and mime type check
# files that should be ignored during extension and MIME type checks

Copilot uses AI. Check for mistakes.
UPLOAD_FILES_WHITELIST = config("UPLOAD_FILES_WHITELIST", default="", cast=Csv())
15 changes: 15 additions & 0 deletions server/mergin/sync/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from flask import current_app
from pathlib import Path

from .config import Configuration


def generate_checksum(file, chunk_size=4096):
"""
Expand Down Expand Up @@ -349,6 +351,8 @@ def has_trailing_space(filepath: str) -> bool:

def is_supported_extension(filepath) -> bool:
"""Check whether file's extension is supported."""
if check_skip_validation(filepath):
return True
ext = os.path.splitext(filepath)[1].lower()
return ext and ext not in FORBIDDEN_EXTENSIONS

Expand Down Expand Up @@ -491,6 +495,15 @@ def is_supported_extension(filepath) -> bool:
".xnk",
}


def check_skip_validation(file_path: str) -> bool:
"""
Check if we can skip validation for this file path.
Some files are allowed even if they have forbidden extension or mime type.
"""
return file_path in Configuration.UPLOAD_FILES_WHITELIST


FORBIDDEN_MIME_TYPES = {
"application/x-msdownload",
"application/x-sh",
Expand All @@ -515,6 +528,8 @@ def is_supported_extension(filepath) -> bool:

def is_supported_type(filepath) -> bool:
"""Check whether the file mimetype is supported."""
if check_skip_validation(filepath):
return True
mime_type = get_mimetype(filepath)
return mime_type.startswith("image/") or mime_type not in FORBIDDEN_MIME_TYPES
Comment on lines 499 to 534
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check_skip_validation compares the passed-in file_path directly against Configuration.UPLOAD_FILES_WHITELIST, but is_supported_type is called with a filesystem path in the upload flow (e.g., temporary_location in server/mergin/sync/models.py). That means the whitelist will never match and MIME type validation will not actually be skipped for the intended project-relative paths. Consider changing the API so is_supported_type/check_skip_validation can check the original project-relative path (e.g., accept both logical_path and local_path, or have the caller decide whether to skip before calling get_mimetype).

Copilot uses AI. Check for mistakes.

Expand Down
46 changes: 46 additions & 0 deletions server/mergin/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@
has_valid_characters,
has_valid_first_character,
check_filename,
is_supported_extension,
is_supported_type,
is_valid_path,
get_x_accel_uri,
wkb2wkt,
has_trailing_space,
check_skip_validation,
)
from ..auth.models import LoginHistory, User
from . import json_headers
Expand Down Expand Up @@ -322,3 +325,46 @@ class TestSchema(Schema):
"size": "disk_usage",
}
assert schema_map == expected_map


def test_check_skip_validation():
ALLOWED_FILES = ["script.js", "config/script.js"]

# We patch the Configuration class attribute directly
with patch("mergin.sync.utils.Configuration.UPLOAD_FILES_WHITELIST", ALLOWED_FILES):

# Test allowed files
for file_path in ALLOWED_FILES:
assert check_skip_validation(file_path)

# Test not allowed files
assert not check_skip_validation("test.py")
assert not check_skip_validation("/some/path/test.py")
assert not check_skip_validation("image.png")


def test_is_supported_extension():
ALLOWED_FILES = ["script.js", "config/script.js"]

with patch("mergin.sync.utils.Configuration.UPLOAD_FILES_WHITELIST", ALLOWED_FILES):
for file_path in ALLOWED_FILES:
assert is_supported_extension(file_path)

# Allowed normal file
assert is_supported_extension("image.png")

# Forbidden file
assert not is_supported_extension("test.js")


def test_mime_type_validation_skip():
ALLOWED_FILES = ["script.js", "config/script.js"]
# Mocking get_mimetype to return forbidden mime type
with patch(
"mergin.sync.utils.get_mimetype", return_value="application/x-python-code"
), patch("mergin.sync.utils.Configuration.UPLOAD_FILES_WHITELIST", ALLOWED_FILES):
for file_path in ALLOWED_FILES:
assert is_supported_extension(file_path)
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_mime_type_validation_skip patches get_mimetype and is meant to verify that MIME type validation is skipped for whitelisted paths, but it currently asserts is_supported_extension(file_path) inside the loop. That doesn't exercise is_supported_type at all, so the test will pass even if MIME skipping is broken. Update the assertions to call is_supported_type(...) for the whitelisted paths (and keep the non-whitelisted negative case).

Suggested change
assert is_supported_extension(file_path)
assert is_supported_type(file_path)

Copilot uses AI. Check for mistakes.

# Should be forbidden
assert not is_supported_type("other.js")
Loading