{"title":"Arbitrary File Read via Jinja2 FileSystemLoader Path Traversal","language":"Python","severity":"High","cwe":"CWE-22","source_lines":[9,10],"flow_lines":[9,11,12,14],"sink_lines":[14],"vulnerable_code":"from flask import Flask, request, render_template_string\nfrom jinja2 import Environment, FileSystemLoader\nimport os\n\napp = Flask(__name__)\n\n@app.route('/iot/device/config')\ndef fetch_device_config():\n    device_id = request.args.get('device_id', 'default')\n    config_theme = request.args.get('theme', 'standard')\n    base_path = '/var/iot/devices/templates'\n    template_dir = os.path.join(base_path, config_theme)\n    env = Environment(loader=FileSystemLoader(template_dir))\n    try:\n        tmpl = env.get_template(f'{device_id}_config.html')\n        return tmpl.render(device_id=device_id)\n    except Exception as e:\n        return f'Configuration not found: {str(e)}', 404","explanation":"The application accepts user-controlled 'device_id' and 'theme' parameters without validation, allowing path traversal sequences. The 'theme' parameter is used to construct the template directory path via os.path.join(), and 'device_id' is used directly in get_template(), enabling attackers to traverse outside the intended directory and read arbitrary files from the filesystem.","remediation":"The fix implements multiple layers of defense: an allowlist for theme values, regex validation on device_id to reject path traversal characters, and os.path.realpath() checks to ensure resolved paths remain within the intended base directory. This prevents both the theme-based directory traversal and the device_id-based template filename traversal.","secure_code":"from flask import Flask, request, render_template_string\nfrom jinja2 import Environment, FileSystemLoader\nimport os\nimport re\n\napp = Flask(__name__)\n\nALLOWED_THEMES = {'standard', 'dark', 'light', 'compact'}\n\n@app.route('/iot/device/config')\ndef fetch_device_config():\n    device_id = request.args.get('device_id', 'default')\n    config_theme = request.args.get('theme', 'standard')\n    base_path = '/var/iot/devices/templates'\n\n    # Validate theme against allowlist\n    if config_theme not in ALLOWED_THEMES:\n        return 'Invalid theme specified', 400\n\n    # Validate device_id contains only safe characters (alphanumeric, hyphens, underscores)\n    if not re.match(r'^[a-zA-Z0-9_\\-]+$', device_id):\n        return 'Invalid device ID format', 400\n\n    # Construct and validate template directory path\n    template_dir = os.path.realpath(os.path.join(base_path, config_theme))\n    if not template_dir.startswith(os.path.realpath(base_path) + os.sep):\n        return 'Invalid template directory', 400\n\n    # Construct and validate full template file path\n    template_file = f'{device_id}_config.html'\n    full_template_path = os.path.realpath(os.path.join(template_dir, template_file))\n    if not full_template_path.startswith(os.path.realpath(template_dir) + os.sep):\n        return 'Invalid template path', 400\n\n    env = Environment(loader=FileSystemLoader(template_dir))\n    try:\n        tmpl = env.get_template(template_file)\n        return tmpl.render(device_id=device_id)\n    except Exception as e:\n        return 'Configuration not found', 404"}