{"title":"Arbitrary File Upload via Extension-Only Validation","language":"Python","severity":"Critical","cwe":"CWE-434","source_lines":[3],"flow_lines":[3,5,8],"sink_lines":[8],"vulnerable_code":"@app.route('/api/v2/iot/firmware', methods=['POST'])\ndef deploy_device_firmware():\n    device_id = request.form.get('device_id')\n    fw_package = request.files['firmware']\n    allowed = ['.bin', '.hex', '.fw']\n    if any(fw_package.filename.endswith(ext) for ext in allowed):\n        storage_path = f'/opt/iot_hub/firmware/{device_id}/{fw_package.filename}'\n        os.makedirs(os.path.dirname(storage_path), exist_ok=True)\n        fw_package.save(storage_path)\n        return jsonify({'status': 'deployed', 'path': storage_path})\n    return jsonify({'error': 'Invalid firmware format'}), 400","explanation":"The code validates file uploads using only extension checking with endswith(), which can be bypassed using double extensions or manipulated filenames like 'malicious.php.bin'. The unsanitized filename is then directly used in the file path construction, allowing path traversal attacks and arbitrary file upload to any location with crafted filenames.","remediation":"The fix addresses multiple vulnerabilities: it uses os.path.splitext() instead of endswith() to properly validate only the final extension, applies werkzeug's secure_filename() to strip path traversal sequences, generates a UUID-based unique filename to prevent name-based attacks, validates the device_id against a strict whitelist pattern, resolves the final path and verifies it remains within the intended base directory, and removes the full filesystem path from the response to prevent information disclosure.","secure_code":"import os\nimport re\nimport uuid\nimport hashlib\nfrom werkzeug.utils import secure_filename\n\nALLOWED_EXTENSIONS = {'.bin', '.hex', '.fw'}\nFIRMWARE_BASE_PATH = '/opt/iot_hub/firmware'\nDEVICE_ID_PATTERN = re.compile(r'^[a-zA-Z0-9_\\-]{1,64}$')\n\n\ndef validate_firmware_extension(filename):\n    \"\"\"Validate that the file has exactly one allowed extension.\"\"\"\n    if not filename or '.' not in filename:\n        return False\n    _, ext = os.path.splitext(filename)\n    return ext.lower() in ALLOWED_EXTENSIONS\n\n\n@app.route('/api/v2/iot/firmware', methods=['POST'])\ndef deploy_device_firmware():\n    device_id = request.form.get('device_id')\n\n    # Validate device_id to prevent path traversal via directory name\n    if not device_id or not DEVICE_ID_PATTERN.match(device_id):\n        return jsonify({'error': 'Invalid device ID'}), 400\n\n    if 'firmware' not in request.files:\n        return jsonify({'error': 'No firmware file provided'}), 400\n\n    fw_package = request.files['firmware']\n\n    # Validate extension using os.path.splitext (not endswith)\n    if not validate_firmware_extension(fw_package.filename):\n        return jsonify({'error': 'Invalid firmware format'}), 400\n\n    # Sanitize the filename to remove path traversal characters\n    safe_filename = secure_filename(fw_package.filename)\n\n    # Re-validate after sanitization (secure_filename may alter the name)\n    if not safe_filename or not validate_firmware_extension(safe_filename):\n        return jsonify({'error': 'Invalid firmware filename'}), 400\n\n    # Generate a unique filename to prevent overwrites and name-based attacks\n    _, ext = os.path.splitext(safe_filename)\n    unique_name = f\"{uuid.uuid4().hex}{ext}\"\n\n    # Construct path safely and verify it's within the allowed directory\n    storage_dir = os.path.join(FIRMWARE_BASE_PATH, device_id)\n    storage_path = os.path.join(storage_dir, unique_name)\n\n    # Resolve to absolute path and verify it stays within base directory\n    resolved_path = os.path.realpath(storage_path)\n    resolved_base = os.path.realpath(FIRMWARE_BASE_PATH)\n    if not resolved_path.startswith(resolved_base + os.sep):\n        return jsonify({'error': 'Invalid storage path'}), 400\n\n    os.makedirs(storage_dir, exist_ok=True)\n    fw_package.save(resolved_path)\n\n    # Return only the firmware ID, not the full filesystem path\n    return jsonify({'status': 'deployed', 'firmware_id': unique_name, 'device_id': device_id})"}