{"title":"TOCTOU Race Condition via os.access() Before File Open","language":"Python","severity":"High","cwe":"CWE-367","source_lines":[5],"flow_lines":[5,6],"sink_lines":[6],"vulnerable_code":"import os\nimport json\n\ndef load_iot_device_config(device_id):\n    config_path = f\"/var/iot/devices/{device_id}/config.json\"\n    if os.access(config_path, os.R_OK):\n        with open(config_path, 'r') as cfg:\n            device_settings = json.load(cfg)\n            return device_settings\n    else:\n        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":"import os\nimport json\n\ndef load_iot_device_config(device_id):\n    config_path = f\"/var/iot/devices/{device_id}/config.json\"\n    \n    # Sanitize device_id to prevent path traversal\n    base_dir = \"/var/iot/devices\"\n    real_base = os.path.realpath(base_dir)\n    real_path = os.path.realpath(config_path)\n    if not real_path.startswith(real_base + os.sep):\n        raise PermissionError(f\"Invalid device path for device {device_id}\")\n    \n    try:\n        # Open the file directly using O_NOFOLLOW to prevent symlink attacks\n        fd = os.open(real_path, os.O_RDONLY | os.O_NOFOLLOW)\n        try:\n            # Verify the opened file is a regular file\n            stat_info = os.fstat(fd)\n            import stat\n            if not stat.S_ISREG(stat_info.st_mode):\n                raise PermissionError(f\"Config for device {device_id} is not a regular file\")\n            \n            with os.fdopen(fd, 'r') as cfg:\n                device_settings = json.load(cfg)\n                return device_settings\n        except:\n            os.close(fd)\n            raise\n    except OSError as e:\n        raise PermissionError(f\"Cannot access config for device {device_id}: {e}\")"}