VYPR
Critical severityNVD Advisory· Published Feb 25, 2026· Updated Feb 25, 2026

Flask-Reuploaded vulnerable to Remote Code Execution via Server-Side Template Injection

CVE-2026-27641

Description

Flask-Reuploaded provides file uploads for Flask. A critical path traversal and extension bypass vulnerability in versions prior to 1.5.0 allows remote attackers to achieve arbitrary file write and remote code execution through Server-Side Template Injection (SSTI). Flask-Reuploaded has been patched in version 1.5.0. Some workarounds are available. Do not pass user input to the name parameter, use auto-generated filenames only, and implement strict input validation if name must be used.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Flask-Reuploaded <1.5.0 allows path traversal and extension bypass via the `name` parameter, leading to arbitrary file write and RCE through SSTI.

Vulnerability

Flask-Reuploaded prior to version 1.5.0 fails to properly sanitize the name parameter in file uploads, allowing path traversal and extension bypass. An attacker can supply a malicious name value containing path separators (e.g., ../../../) to write files outside the intended upload directory, or bypass extension validation to upload executable files such as templates that lead to Server-Side Template Injection (SSTI) [1][2][3].

Exploitation

The vulnerability is exploitable remotely without authentication, requiring only the ability to send a crafted multipart request to an endpoint that uses the library's save() function with user-controlled name parameter. No special network position is needed; the attack surface is any Flask application using Flask-Reuploaded that passes unsanitized user input to the name argument [3].

Impact

Successful exploitation achieves arbitrary file write and, by uploading a malicious template, remote code execution through SSTI. This grants the attacker full control over the server, potentially leading to data theft, service disruption, or further lateral movement [2][3].

Mitigation

Flask-Reuploaded version 1.5.0 patches the issue by sanitizing filenames with secure_filename() and re-validating extensions after name override [1][4]. Workarounds include avoiding user input in the name parameter, using auto-generated filenames, and implementing strict input validation if name must be used [3].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
flask-reuploadedPyPI
< 1.5.01.5.0

Affected products

2

Patches

1
d64c6b2f71cb

Security fix: prevent path traversal and extension bypass

https://github.com/jugmac00/flask-reuploadedJürgen GmachFeb 20, 2026via ghsa
3 files changed · +302 5
  • CHANGES.rst+17 3 modified
    @@ -5,11 +5,25 @@ Changelog
     ------------------
     - drop support for Python 3.8 and 3.9
     - add support for Python 3.13
    -
    -1.4.1 (unreleased)
    -------------------
     - migrate from setup.py to pyproject.toml configuration
     - fix doc building on read the docs
    +- **SECURITY FIX**: Fix critical path traversal and extension bypass vulnerability (CVE pending, CVSS 9.8)
    +
    +  - Apply ``secure_filename()`` to the ``name`` parameter to prevent path traversal attacks
    +  - Re-validate file extension after ``name`` override to prevent extension bypass
    +  - Add path containment check to ensure files are saved within the upload directory
    +  - Sanitize folder component when extracted from ``name`` parameter
    +
    +  **Impact**: This vulnerability allowed remote attackers to write files to arbitrary locations
    +  on the filesystem and bypass extension restrictions, potentially leading to remote code
    +  execution via Server-Side Template Injection (SSTI) in Flask applications.
    +
    +  **Credit**: Jaron Cabral (Cal Poly Humboldt) for discovery and reporting
    +
    +  **Recommendation**: All users should upgrade to this version immediately. Do not pass
    +  user-controlled input to the ``name`` parameter in older versions.
    +
    +
     
     1.4.0 (2023.10.03)
     ------------------
    
  • src/flask_uploads/flask_uploads.py+39 2 modified
    @@ -309,8 +309,18 @@ def save(
             if not isinstance(storage, FileStorage):
                 raise TypeError("storage must be a werkzeug.FileStorage")
     
    +        # Track if name ends with dot before any processing
    +        name_ends_with_dot = name is not None and name.rstrip().endswith('.')
    +
             if folder is None and name is not None and "/" in name:
                 folder, name = os.path.split(name)
    +            # Check again after split
    +            name_ends_with_dot = name.rstrip().endswith('.')
    +            # Sanitize folder and name extracted from name parameter
    +            if folder:
    +                folder = secure_filename(folder)
    +            if name:
    +                name = secure_filename(name)
             if storage.filename is None:
                 raise ValueError("Filename must not be empty!")
             basename = self.get_basename(storage.filename)
    @@ -319,12 +329,27 @@ def save(
                 raise UploadNotAllowed()
     
             if name:
    -            if name.endswith('.'):
    -                basename = name + extension(basename)
    +            # Sanitize name parameter to prevent path traversal
    +            name = secure_filename(name)
    +            if not name:
    +                raise ValueError("Invalid filename after sanitization")
    +
    +            if name_ends_with_dot and not name.endswith('.'):
    +                # Restore the dot if it was removed by secure_filename
    +                basename = name + '.' + extension(basename)
                 else:
                     basename = name
     
    +            # Re-validate extension after name override
    +            ext = extension(basename)
    +            if ext and not self.extension_allowed(ext):
    +                raise UploadNotAllowed(
    +                    f"File extension '{ext}' is not allowed"
    +                )
    +
             if folder:
    +            # Additional sanitization of folder parameter
    +            folder = secure_filename(folder)
                 target_folder = os.path.join(self.config.destination, folder)
             else:
                 target_folder = self.config.destination
    @@ -334,6 +359,18 @@ def save(
                 basename = self.resolve_conflict(target_folder, basename)
     
             target = os.path.join(target_folder, basename)
    +
    +        # Verify path containment to prevent directory traversal
    +        target_real = os.path.realpath(target)
    +        dest_real = os.path.realpath(self.config.destination)
    +        if not (
    +            target_real.startswith(dest_real + os.sep) or
    +            target_real == dest_real
    +        ):
    +            raise ValueError(
    +                "Security: Path traversal attempt detected"
    +            )
    +
             storage.save(target)
             if folder:
                 return posixpath.join(folder, basename)
    
  • tests/test_flask_reuploaded.py+246 0 modified
    @@ -6,13 +6,16 @@
     
     import os
     import os.path
    +import shutil
    +import tempfile
     from unittest.mock import Mock
     from unittest.mock import patch
     
     import pytest
     from flask import Flask
     from flask import url_for
     from flask_uploads import ALL
    +from flask_uploads import IMAGES
     from flask_uploads import AllExcept
     from flask_uploads import TestingFileStorage
     from flask_uploads import UploadConfiguration
    @@ -394,3 +397,246 @@ def test_configure_for_set_throws_runtimeerror() -> None:
         app = Flask(__name__)
         with pytest.raises(RuntimeError):
             config_for_set(upload_set, app)
    +
    +
    +class TestSecurityFixes:
    +    """Tests for security vulnerability fixes.
    +
    +    These tests verify that path traversal and extension bypass vulnerabilities
    +    have been properly fixed.
    +    """
    +
    +    def test_path_traversal_prevention_via_name_parameter(self) -> None:
    +        """Verify path traversal via `name` is prevented."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="safe.txt")
    +
    +            result = uset.save(tfs, name="../../../etc/passwd")
    +
    +            assert "../" not in result
    +            assert "passwd" in result
    +            assert tfs.saved is not None
    +            assert "passwd" in tfs.saved
    +            assert os.path.realpath(tfs.saved).startswith(
    +                os.path.realpath(tmpdir)
    +            )
    +
    +    def test_absolute_path_prevention_via_name_parameter(self) -> None:
    +        """Verify absolute paths in `name` are sanitized."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="safe.txt")
    +
    +            result = uset.save(tfs, name="/etc/passwd")
    +
    +            assert "passwd" in result
    +            assert tfs.saved is not None
    +            assert "passwd" in tfs.saved
    +
    +    def test_extension_bypass_prevention_via_name_parameter(self) -> None:
    +        """Verify extension validation cannot be bypassed via `name`."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("photos", IMAGES)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="legitimate.jpg")
    +
    +            with pytest.raises(UploadNotAllowed) as exc_info:
    +                uset.save(tfs, name="backdoor.py")
    +
    +            assert "py" in str(exc_info.value).lower()
    +
    +    def test_extension_bypass_with_double_extension(self) -> None:
    +        """Verify double extensions don't bypass validation."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("photos", IMAGES)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="safe.jpg")
    +
    +            result = uset.save(tfs, name="backdoor.php.jpg")
    +            assert ".jpg" in result
    +
    +    def test_folder_extraction_sanitization(self) -> None:
    +        """Verify folder extracted from `name` is sanitized."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="test.txt")
    +
    +            result = uset.save(tfs, name="../../tmp/file.txt")
    +
    +            assert "../" not in result
    +            assert tfs.saved is not None
    +            assert os.path.realpath(tfs.saved).startswith(
    +                os.path.realpath(tmpdir)
    +            )
    +            assert "file.txt" in result
    +
    +    def test_explicit_folder_parameter_sanitization(self) -> None:
    +        """Verify explicit `folder` parameter is sanitized."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="test.txt")
    +
    +            result = uset.save(tfs, folder="../../tmp")
    +
    +            assert "../" not in result
    +            assert tfs.saved is not None
    +            assert os.path.realpath(tfs.saved).startswith(
    +                os.path.realpath(tmpdir)
    +            )
    +
    +    def test_path_containment_check(self) -> None:
    +        """Verify final path is contained within upload directory."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="test.txt")
    +
    +            uset.save(tfs, name="../../../../../../../../tmp/escape.txt")
    +
    +            assert tfs.saved is not None
    +            real_saved = os.path.realpath(tfs.saved)
    +            real_upload = os.path.realpath(tmpdir)
    +            assert real_saved.startswith(real_upload)
    +
    +    def test_empty_name_after_sanitization(self) -> None:
    +        """Verify names that become empty after sanitization are rejected."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="test.txt")
    +
    +            with pytest.raises(ValueError) as exc_info:
    +                uset.save(tfs, name="...")
    +
    +            assert "sanitization" in str(exc_info.value).lower()
    +
    +    def test_windows_path_separators(self) -> None:
    +        """Verify Windows-style path separators are sanitized."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="test.txt")
    +
    +            result = uset.save(tfs, name="..\\..\\temp\\evil.txt")
    +
    +            assert "\\" not in result
    +            assert tfs.saved is not None
    +            assert os.path.realpath(tfs.saved).startswith(
    +                os.path.realpath(tmpdir)
    +            )
    +
    +    def test_legitimate_subfolder_still_works(self) -> None:
    +        """Verify legitimate subfolder usage still works."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="photo.jpg")
    +
    +            result = uset.save(tfs, name="users/avatar.jpg")
    +
    +            assert result == "users/avatar.jpg"
    +            assert tfs.saved is not None
    +            assert "users" in tfs.saved
    +            assert "avatar.jpg" in tfs.saved
    +
    +    def test_legitimate_custom_name_still_works(self) -> None:
    +        """Verify legitimate custom names still work."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="upload.txt")
    +
    +            result = uset.save(tfs, name="renamed_file.txt")
    +
    +            assert result == "renamed_file.txt"
    +            assert tfs.saved is not None
    +            assert "renamed_file.txt" in tfs.saved
    +
    +    def test_legitimate_name_with_extension_placeholder(self) -> None:
    +        """Verify trailing dot preserves extension."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("photos", IMAGES)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="photo.jpg")
    +
    +            result = uset.save(tfs, name="image_123.")
    +
    +            assert result == "image_123.jpg"
    +            assert tfs.saved is not None
    +            assert "image_123.jpg" in tfs.saved
    +
    +    def test_combined_attack_prevention(self) -> None:
    +        """Verify combined path traversal + extension bypass is prevented."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("photos", IMAGES)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="payload.jpg")
    +
    +            with pytest.raises(UploadNotAllowed):
    +                uset.save(tfs, name="../templates/rce.html")
    +
    +    def test_null_byte_injection(self) -> None:
    +        """Verify null byte injection is sanitized."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="test.txt")
    +
    +            result = uset.save(tfs, name="file.txt\x00.jpg")
    +
    +            assert "\x00" not in result
    +            assert tfs.saved is not None
    +            assert os.path.realpath(tfs.saved).startswith(
    +                os.path.realpath(tmpdir)
    +            )
    +
    +    def test_special_characters_sanitization(self) -> None:
    +        """Verify special characters are sanitized."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="test.txt")
    +
    +            result = uset.save(tfs, name='file<>:"|?*.txt')
    +
    +            for char in '<>:"|?*':
    +                assert char not in result
    +            assert tfs.saved is not None
    +            assert os.path.realpath(tfs.saved).startswith(
    +                os.path.realpath(tmpdir)
    +            )
    +
    +    def test_name_already_ends_with_dot(self) -> None:
    +        """Verify trailing dot keeps extension."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            uset = UploadSet("files", ALL)
    +            uset._config = Config(tmpdir)
    +            tfs = TestingFileStorage(filename="photo.jpg")
    +
    +            result = uset.save(tfs, name="myfile.")
    +
    +            assert result == "myfile.jpg"
    +            assert tfs.saved is not None
    +            assert "myfile.jpg" in tfs.saved
    +
    +    def test_symlink_path_traversal_prevention(self) -> None:
    +        """Verify symlinks cannot be used to escape upload directory."""
    +        with tempfile.TemporaryDirectory() as tmpdir:
    +            outside_dir = tempfile.mkdtemp()
    +            try:
    +                symlink_path = os.path.join(tmpdir, "link")
    +                os.symlink(outside_dir, symlink_path)
    +
    +                uset = UploadSet("files", ALL)
    +                uset._config = Config(tmpdir)
    +                tfs = TestingFileStorage(filename="test.txt")
    +
    +                with pytest.raises(ValueError, match="Path traversal"):
    +                    uset.save(tfs, folder="link", name="../../escape.txt")
    +            finally:
    +                shutil.rmtree(outside_dir, ignore_errors=True)
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.