{"title":"TOCTOU Race Condition in File Existence Check","language":"Python","severity":"Medium","cwe":"CWE-367","source_lines":[5],"flow_lines":[5,9,11],"sink_lines":[9,11],"vulnerable_code":"import os\nimport boto3\n\ndef sync_model_weights_to_s3(local_weights_path, s3_bucket, model_id):\n    if not os.path.exists(local_weights_path):\n        raise FileNotFoundError(f\"Model weights not found: {local_weights_path}\")\n    s3_client = boto3.client('s3')\n    s3_key = f\"ml-models/{model_id}/weights.h5\"\n    with open(local_weights_path, 'rb') as weight_file:\n        s3_client.upload_fileobj(weight_file, s3_bucket, s3_key)\n    os.remove(local_weights_path)\n    return f\"s3://{s3_bucket}/{s3_key}\"","explanation":"This code has a TOCTOU (Time-of-check Time-of-use) race condition. The file existence is checked at line 5, but between this check and the actual file operations (open at line 9, remove at line 11), an attacker could delete, replace, or create a symbolic link to the file, causing the wrong file to be uploaded or deleted.","remediation":"The fix eliminates the TOCTOU race condition by removing the separate os.path.exists() check and instead opening the file directly using os.open() with the O_NOFOLLOW flag to prevent symlink attacks. The file descriptor is used for all subsequent operations, and os.fstat() is called on the already-opened descriptor to verify it is a regular file, ensuring that what is checked is exactly what is used. The fd ownership is transferred to os.fdopen() before upload to prevent double-close scenarios, and os.remove() is wrapped in a try/except to handle edge cases gracefully after a successful upload.","secure_code":"import os\nimport stat\nimport boto3\n\ndef sync_model_weights_to_s3(local_weights_path, s3_bucket, model_id):\n    s3_client = boto3.client('s3')\n    s3_key = f\"ml-models/{model_id}/weights.h5\"\n\n    # Open the file directly without a prior existence check to eliminate TOCTOU.\n    # Use O_NOFOLLOW to prevent symlink attacks.\n    try:\n        fd = os.open(local_weights_path, os.O_RDONLY | os.O_NOFOLLOW)\n    except FileNotFoundError:\n        raise FileNotFoundError(f\"Model weights not found: {local_weights_path}\")\n    except OSError as e:\n        raise OSError(f\"Cannot open model weights (possible symlink attack): {local_weights_path}\") from e\n\n    try:\n        # Verify the opened path is a regular file using the file descriptor\n        stat_result = os.fstat(fd)\n        if not stat.S_ISREG(stat_result.st_mode):\n            raise ValueError(f\"Path is not a regular file: {local_weights_path}\")\n\n        with os.fdopen(fd, 'rb') as weight_file:\n            fd = -1  # fd is now owned by weight_file; prevent double-close\n            s3_client.upload_fileobj(weight_file, s3_bucket, s3_key)\n    except Exception:\n        if fd >= 0:\n            os.close(fd)\n        raise\n\n    # Safely remove the local file after successful upload.\n    try:\n        os.remove(local_weights_path)\n    except OSError:\n        pass  # File may have already been removed; upload succeeded so this is acceptable\n\n    return f\"s3://{s3_bucket}/{s3_key}\""}