{"title":"Path Traversal via pathlib.Path.resolve() Symlink Bypass","language":"Python","severity":"High","cwe":"CWE-22","source_lines":[10],"flow_lines":[10,11,12,15],"sink_lines":[15],"vulnerable_code":"from pathlib import Path\nfrom flask import Flask, send_file, request\n\napp = Flask(__name__)\nMODEL_STORAGE = \"/var/ml/trained_models\"\n\n@app.route('/api/v2/model/export')\ndef export_trained_model():\n    model_name = request.args.get('model_id', 'default.h5')\n    model_path = Path(MODEL_STORAGE) / model_name\n    resolved = model_path.resolve()\n    if not str(resolved).startswith(MODEL_STORAGE):\n        return {\"error\": \"Invalid model path\"}, 403\n    return send_file(resolved, as_attachment=True)","explanation":"The vulnerability occurs because user-controlled input from request.args.get('model_id') flows directly into Path construction and file operations. Although resolve() is used for validation, an attacker can create a symlink within the MODEL_STORAGE directory pointing to sensitive files outside it. The validation checks if the resolved path starts with MODEL_STORAGE, but this can be bypassed if a symlink is placed inside MODEL_STORAGE that points elsewhere, allowing arbitrary file read via send_file().","remediation":"The fix applies defense-in-depth: it validates the model_id against a strict whitelist regex that only allows safe filename characters and known model extensions, explicitly rejects symlinks before resolution to prevent symlink-based bypasses, and resolves the MODEL_STORAGE base path itself before comparison to ensure consistent prefix checking with a trailing separator.","secure_code":"from pathlib import Path\nimport os\nimport re\nfrom flask import Flask, send_file, request\n\napp = Flask(__name__)\nMODEL_STORAGE = \"/var/ml/trained_models\"\n\n# Whitelist pattern for model IDs: alphanumeric, hyphens, underscores, dots, no path separators\nMODEL_ID_PATTERN = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9._-]*\\.(?:h5|pt|pkl|onnx|safetensors)$')\n\n@app.route('/api/v2/model/export')\ndef export_trained_model():\n    model_name = request.args.get('model_id', 'default.h5')\n    \n    # Validate model_id against whitelist pattern (no path separators, no dots sequences)\n    if not MODEL_ID_PATTERN.match(model_name):\n        return {\"error\": \"Invalid model ID format\"}, 400\n    \n    # Reject any path separator characters explicitly\n    if os.sep in model_name or '/' in model_name or '\\\\' in model_name:\n        return {\"error\": \"Invalid model ID\"}, 400\n    \n    # Reject '..' sequences\n    if '..' in model_name:\n        return {\"error\": \"Invalid model ID\"}, 400\n    \n    model_path = Path(MODEL_STORAGE) / model_name\n    \n    # Check that the file is not a symlink (prevent symlink-based bypass)\n    if model_path.is_symlink():\n        return {\"error\": \"Invalid model path: symlinks not allowed\"}, 403\n    \n    # Resolve and verify the path is within MODEL_STORAGE\n    resolved = model_path.resolve()\n    storage_resolved = Path(MODEL_STORAGE).resolve()\n    \n    if not str(resolved).startswith(str(storage_resolved) + os.sep) and resolved != storage_resolved:\n        return {\"error\": \"Invalid model path\"}, 403\n    \n    # Verify file exists and is a regular file\n    if not resolved.is_file():\n        return {\"error\": \"Model not found\"}, 404\n    \n    return send_file(resolved, as_attachment=True)"}