# Arbitrary File Read via Jinja2 FileSystemLoader Path Traversal

Language: Python
Severity: High
CWE: CWE-22

## Source
9-10

## Flow
9-11-12-14

## Sink
14

## Vulnerable Code
```python
from flask import Flask, request, render_template_string
from jinja2 import Environment, FileSystemLoader
import os

app = Flask(__name__)

@app.route('/iot/device/config')
def fetch_device_config():
    device_id = request.args.get('device_id', 'default')
    config_theme = request.args.get('theme', 'standard')
    base_path = '/var/iot/devices/templates'
    template_dir = os.path.join(base_path, config_theme)
    env = Environment(loader=FileSystemLoader(template_dir))
    try:
        tmpl = env.get_template(f'{device_id}_config.html')
        return tmpl.render(device_id=device_id)
    except Exception as e:
        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
```python
from flask import Flask, request, render_template_string
from jinja2 import Environment, FileSystemLoader
import os
import re

app = Flask(__name__)

ALLOWED_THEMES = {'standard', 'dark', 'light', 'compact'}

@app.route('/iot/device/config')
def fetch_device_config():
    device_id = request.args.get('device_id', 'default')
    config_theme = request.args.get('theme', 'standard')
    base_path = '/var/iot/devices/templates'

    # Validate theme against allowlist
    if config_theme not in ALLOWED_THEMES:
        return 'Invalid theme specified', 400

    # Validate device_id contains only safe characters (alphanumeric, hyphens, underscores)
    if not re.match(r'^[a-zA-Z0-9_\-]+$', device_id):
        return 'Invalid device ID format', 400

    # Construct and validate template directory path
    template_dir = os.path.realpath(os.path.join(base_path, config_theme))
    if not template_dir.startswith(os.path.realpath(base_path) + os.sep):
        return 'Invalid template directory', 400

    # Construct and validate full template file path
    template_file = f'{device_id}_config.html'
    full_template_path = os.path.realpath(os.path.join(template_dir, template_file))
    if not full_template_path.startswith(os.path.realpath(template_dir) + os.sep):
        return 'Invalid template path', 400

    env = Environment(loader=FileSystemLoader(template_dir))
    try:
        tmpl = env.get_template(template_file)
        return tmpl.render(device_id=device_id)
    except Exception as e:
        return 'Configuration not found', 404
```
