# TOCTOU Race Condition in File Permission Checks

Language: Python
Severity: High
CWE: CWE-367

## Source
4

## Flow
4-5-6-7-10

## Sink
10

## Vulnerable Code
```python
import os
import boto3

def sync_credentials_to_s3(cred_file_path, bucket_name):
    if not os.path.exists(cred_file_path):
        raise FileNotFoundError("Credential file missing")
    file_stats = os.stat(cred_file_path)
    if file_stats.st_mode & 0o077:
        raise PermissionError("Insecure permissions detected")
    s3_client = boto3.client('s3')
    with open(cred_file_path, 'rb') as cred_data:
        s3_client.upload_fileobj(cred_data, bucket_name, 'secrets/aws_creds.json')
    return {"status": "uploaded", "size": file_stats.st_size}
```

## Explanation

This code exhibits a Time-of-Check-Time-of-Use (TOCTOU) race condition vulnerability. The file permissions are checked at line 7 using os.stat(), but the file is opened at line 10, creating a window where an attacker can replace the validated file with a malicious one or modify its permissions between the check and use.

## Remediation

The fix eliminates the TOCTOU race condition by opening the file first with os.open() using O_NOFOLLOW to reject symlinks, then performing permission checks on the already-opened file descriptor using os.fstat(). The same file descriptor is then used for reading and uploading, ensuring the checked file is the same file that gets uploaded. Additionally, the code verifies the file is a regular file to prevent device file attacks.

## Secure Code
```python
import os
import boto3
import stat
import tempfile
import shutil

def sync_credentials_to_s3(cred_file_path, bucket_name):
    # Open the file first using a file descriptor to eliminate TOCTOU race condition
    try:
        fd = os.open(cred_file_path, os.O_RDONLY | os.O_NOFOLLOW)
    except FileNotFoundError:
        raise FileNotFoundError("Credential file missing")
    except OSError as e:
        raise OSError(f"Cannot open credential file: {e}")
    
    try:
        # Check permissions on the already-opened file descriptor
        file_stats = os.fstat(fd)
        
        # Ensure it's a regular file (not a symlink, device, etc.)
        if not stat.S_ISREG(file_stats.st_mode):
            raise PermissionError("Credential path is not a regular file")
        
        # Check that no group or other permissions are set
        if file_stats.st_mode & 0o077:
            raise PermissionError("Insecure permissions detected")
        
        # Use the already-opened file descriptor for reading
        # This ensures we read the same file we checked permissions on
        cred_data = os.fdopen(fd, 'rb')
        fd = -1  # Mark fd as consumed by fdopen
        
        try:
            s3_client = boto3.client('s3')
            s3_client.upload_fileobj(cred_data, bucket_name, 'secrets/aws_creds.json')
        finally:
            cred_data.close()
    finally:
        if fd >= 0:
            os.close(fd)
    
    return {"status": "uploaded", "size": file_stats.st_size}
```
