# TOCTOU Race Condition in File Existence Check and Open

Language: Python
Severity: High
CWE: CWE-367

## Source
7

## Flow
7-8-9

## Sink
9

## Vulnerable Code
```python
import os
import json
import time

def load_iot_device_config(device_id):
    config_path = f"/var/iot/devices/{device_id}/config.json"
    if os.path.exists(config_path):
        time.sleep(0.1)
        with open(config_path, 'r') as cfg:
            device_settings = json.load(cfg)
            return device_settings
    else:
        return {"error": "Device configuration not found"}

def apply_device_settings(device_id):
    settings = load_iot_device_config(device_id)
    if "error" not in settings:
        execute_device_commands(settings.get("commands", []))
```

## Explanation

This code has a Time-of-Check Time-of-Use (TOCTOU) race condition. Between checking if the file exists (line 7) and opening it (line 9), an attacker can delete the legitimate file and replace it with a symlink to a sensitive file, causing the application to read unauthorized data or execute malicious commands that get passed to execute_device_commands(). The deliberate time.sleep(0.1) on line 8 further widens the race window, making exploitation significantly easier.

## Remediation

The fix eliminates the TOCTOU race condition by removing the separate os.path.exists() check and instead opening the file directly with os.open() using O_NOFOLLOW to reject symlinks. After opening, os.fstat() is used on the file descriptor to verify it is a regular file with proper ownership, ensuring the checks are performed on the actual opened file rather than the path. The deliberate sleep is also removed. Input validation for device_id is added to prevent path traversal attacks.

## Secure Code
```python
import os
import json
import stat

def load_iot_device_config(device_id):
    # Sanitize device_id to prevent path traversal
    if not device_id.isalnum() and not all(c.isalnum() or c in ('-', '_') for c in device_id):
        return {"error": "Invalid device ID"}

    config_path = f"/var/iot/devices/{device_id}/config.json"

    try:
        # Open the file directly without a separate existence check (atomic operation)
        fd = os.open(config_path, os.O_RDONLY | os.O_NOFOLLOW)

        # Verify the opened file descriptor points to a regular file
        file_stat = os.fstat(fd)
        if not stat.S_ISREG(file_stat.st_mode):
            os.close(fd)
            return {"error": "Device configuration is not a regular file"}

        # Verify ownership and permissions on the opened file
        if file_stat.st_uid != 0:
            os.close(fd)
            return {"error": "Device configuration has invalid ownership"}

        with os.fdopen(fd, 'r') as cfg:
            device_settings = json.load(cfg)
            return device_settings
    except (OSError, IOError):
        return {"error": "Device configuration not found"}
    except json.JSONDecodeError:
        return {"error": "Device configuration is malformed"}

def apply_device_settings(device_id):
    settings = load_iot_device_config(device_id)
    if "error" not in settings:
        execute_device_commands(settings.get("commands", []))
```
