# TOCTOU Race Condition via os.access() Before File Open

Language: Python
Severity: High
CWE: CWE-367

## Source
5

## Flow
5-6

## Sink
6

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

def load_iot_device_config(device_id):
    config_path = f"/var/iot/devices/{device_id}/config.json"
    if os.access(config_path, os.R_OK):
        with open(config_path, 'r') as cfg:
            device_settings = json.load(cfg)
            return device_settings
    else:
        raise PermissionError(f"Cannot access config for device {device_id}")
```

## Explanation

This code exhibits a classic Time-of-Check-Time-of-Use (TOCTOU) race condition. Between the os.access() check on line 5 and the open() call on line 6, an attacker can replace the legitimate config file with a symlink pointing to a sensitive file, allowing unauthorized file access despite the permission check.

## Remediation

The fix eliminates the TOCTOU race condition by removing the os.access() check and instead opening the file directly with os.O_NOFOLLOW to reject symlinks, then using os.fstat() on the already-opened file descriptor to verify it is a regular file. Additionally, path traversal is prevented by resolving the real path and ensuring it stays within the expected base directory.

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

def load_iot_device_config(device_id):
    config_path = f"/var/iot/devices/{device_id}/config.json"
    
    # Sanitize device_id to prevent path traversal
    base_dir = "/var/iot/devices"
    real_base = os.path.realpath(base_dir)
    real_path = os.path.realpath(config_path)
    if not real_path.startswith(real_base + os.sep):
        raise PermissionError(f"Invalid device path for device {device_id}")
    
    try:
        # Open the file directly using O_NOFOLLOW to prevent symlink attacks
        fd = os.open(real_path, os.O_RDONLY | os.O_NOFOLLOW)
        try:
            # Verify the opened file is a regular file
            stat_info = os.fstat(fd)
            import stat
            if not stat.S_ISREG(stat_info.st_mode):
                raise PermissionError(f"Config for device {device_id} is not a regular file")
            
            with os.fdopen(fd, 'r') as cfg:
                device_settings = json.load(cfg)
                return device_settings
        except:
            os.close(fd)
            raise
    except OSError as e:
        raise PermissionError(f"Cannot access config for device {device_id}: {e}")
```
