{"title":"Insecure Multipart Form File Upload Leading to Polyglot File Execution via MIME Sniffing Bypass","language":"Python","severity":"Critical","cwe":"CWE-434","source_lines":[9],"flow_lines":[9,11],"sink_lines":[11],"vulnerable_code":"from flask import Flask, request, send_from_directory\nimport os\napp = Flask(__name__)\nUPLOAD_ZONE = '/var/www/drone_telemetry/uploads'\ndef store_telemetry_artifact(req_file):\n    declared_mime = req_file.content_type\n    raw_hdr = req_file.stream.read(16)\n    req_file.stream.seek(0)\n    artifact_label = req_file.filename\n    dest_path = os.path.join(UPLOAD_ZONE, artifact_label)\n    req_file.save(dest_path)\n    return send_from_directory(UPLOAD_ZONE, artifact_label, mimetype=declared_mime)\n@app.route('/telemetry/upload', methods=['POST'])\ndef ingest_payload():\n    pkg = request.files.get('artifact')\n    return store_telemetry_artifact(pkg)","explanation":"The code accepts an attacker-controlled filename (line 8, req_file.filename) and uses it unsanitized in os.path.join (line 10), allowing path traversal (e.g., ../../shell.php). The file is then saved (line 11) to the web-accessible upload directory without any extension or content validation. Additionally, the client-supplied Content-Type header is trusted directly as declared_mime and passed into send_from_directory, while the raw header bytes (raw_hdr) are read but never actually validated or enforced — serving as a false-security decoy. This allows an attacker to upload a polyglot file (e.g., JPEG magic bytes + PHP shell) named shell.php that gets stored and later executed by the server or MIME-sniffed by browsers.","remediation":"The fix addresses all identified vulnerabilities: (1) the filename is sanitized with secure_filename() and a UUID-based rename eliminates path traversal and attacker-controlled filenames; (2) a strict extension allowlist replaces the unrestricted upload, rejecting executable file types like .php; (3) magic byte validation ensures file content matches its declared type, defeating polyglot attacks; (4) the server determines the MIME type from the allowlist instead of trusting the client-supplied Content-Type; (5) X-Content-Type-Options: nosniff and Content-Disposition: attachment headers prevent browser MIME sniffing and force download; (6) os.path.realpath canonicalization confirms the final path stays within UPLOAD_ZONE.","secure_code":"from flask import Flask, request, send_from_directory, abort\nimport os\nimport uuid\nfrom werkzeug.utils import secure_filename\n\napp = Flask(__name__)\nUPLOAD_ZONE = '/var/www/drone_telemetry/uploads'\n\n# Allowed extensions and their corresponding safe MIME types\nALLOWED_EXTENSIONS = {\n    '.log': 'text/plain',\n    '.txt': 'text/plain',\n    '.csv': 'text/csv',\n    '.json': 'application/json',\n    '.bin': 'application/octet-stream',\n    '.hex': 'application/octet-stream',\n}\n\n# Magic byte signatures for additional validation\nMAGIC_BYTES = {\n    '.json': [b'{', b'['],\n    '.csv': None,\n    '.log': None,\n    '.txt': None,\n    '.bin': None,\n    '.hex': None,\n}\n\n\ndef _validate_extension(filename):\n    \"\"\"Return the lowercased extension if it is in the allowlist, else None.\"\"\"\n    _, ext = os.path.splitext(filename)\n    ext = ext.lower()\n    if ext in ALLOWED_EXTENSIONS:\n        return ext\n    return None\n\n\ndef _validate_magic_bytes(stream, ext):\n    \"\"\"Check file magic bytes against known signatures for the extension.\"\"\"\n    expected = MAGIC_BYTES.get(ext)\n    if expected is None:\n        return True\n    header = stream.read(16)\n    stream.seek(0)\n    return any(header.lstrip().startswith(sig) for sig in expected)\n\n\ndef store_telemetry_artifact(req_file):\n    # 1. Sanitize the filename to prevent path traversal\n    safe_name = secure_filename(req_file.filename)\n    if not safe_name:\n        abort(400, description='Invalid filename.')\n\n    # 2. Validate extension against allowlist\n    ext = _validate_extension(safe_name)\n    if ext is None:\n        abort(400, description=f'File type not allowed. Accepted: {list(ALLOWED_EXTENSIONS.keys())}')\n\n    # 3. Validate magic bytes match the declared extension\n    if not _validate_magic_bytes(req_file.stream, ext):\n        abort(400, description='File content does not match its declared type.')\n\n    # 4. Generate a unique filename to avoid overwrites and name-based attacks\n    unique_name = f\"{uuid.uuid4().hex}{ext}\"\n    dest_path = os.path.join(UPLOAD_ZONE, unique_name)\n\n    # 5. Resolve the final path and ensure it is still within UPLOAD_ZONE\n    real_dest = os.path.realpath(dest_path)\n    real_upload_zone = os.path.realpath(UPLOAD_ZONE)\n    if not real_dest.startswith(real_upload_zone + os.sep):\n        abort(400, description='Invalid file path.')\n\n    # 6. Save the file\n    req_file.save(real_dest)\n\n    # 7. Serve back with a safe, server-determined MIME type and\n    #    X-Content-Type-Options: nosniff to prevent browser MIME sniffing\n    safe_mime = ALLOWED_EXTENSIONS[ext]\n    response = send_from_directory(real_upload_zone, unique_name, mimetype=safe_mime)\n    response.headers['X-Content-Type-Options'] = 'nosniff'\n    response.headers['Content-Disposition'] = f'attachment; filename=\"{safe_name}\"'\n    return response\n\n\n@app.route('/telemetry/upload', methods=['POST'])\ndef ingest_payload():\n    pkg = request.files.get('artifact')\n    if pkg is None:\n        abort(400, description='No artifact file provided.')\n    return store_telemetry_artifact(pkg)"}