{"title":"TOCTOU Race Condition in File Creation and Permission Checks","language":"Python","severity":"High","cwe":"CWE-367","source_lines":[8],"flow_lines":[8,9,11],"sink_lines":[11],"vulnerable_code":"import os\nimport boto3\n\ndef deploy_lambda_config(config_name, s3_bucket, lambda_arn):\n    local_cfg = f\"/tmp/lambda_configs/{config_name}.json\"\n    if not os.path.exists(local_cfg):\n        s3 = boto3.client('s3')\n        s3.download_file(s3_bucket, f\"configs/{config_name}.json\", local_cfg)\n    if os.stat(local_cfg).st_uid == os.getuid():\n        with open(local_cfg, 'r') as f:\n            config_data = f.read()\n        lambda_client = boto3.client('lambda')\n        lambda_client.update_function_configuration(FunctionName=lambda_arn, Environment={'Variables': {'CONFIG': config_data}})\n        return True\n    return False","explanation":"This is a TOCTOU (Time-of-Check-Time-of-Use) race condition vulnerability. The code checks file ownership at line 8 (os.stat) but reads the file at line 9 (open), creating a window where an attacker can replace the file between the ownership check and the file read operation. The malicious config_data is then deployed to the Lambda function at line 11, allowing configuration injection.","remediation":"The fix eliminates the TOCTOU race condition by using a freshly created temporary file with restrictive permissions (0o600), downloading directly to it, then performing ownership and file type checks on the open file descriptor (using os.fstat on the fd) rather than on the file path. Reading is done from the same verified file descriptor, ensuring no window exists for an attacker to swap the file between check and use. The O_NOFOLLOW flag prevents symlink attacks.","secure_code":"import os\nimport boto3\nimport tempfile\nimport stat\n\ndef deploy_lambda_config(config_name, s3_bucket, lambda_arn):\n    config_dir = \"/tmp/lambda_configs\"\n    os.makedirs(config_dir, mode=0o700, exist_ok=True)\n    \n    local_cfg = f\"/tmp/lambda_configs/{config_name}.json\"\n    \n    fd = None\n    tmp_path = None\n    try:\n        fd = tempfile.mkstemp(dir=config_dir, prefix=f\"{config_name}_\", suffix=\".json\")\n        tmp_path = fd[1]\n        os.fchmod(fd[0], stat.S_IRUSR | stat.S_IWUSR)\n        os.close(fd[0])\n        fd = None\n        \n        s3 = boto3.client('s3')\n        s3.download_file(s3_bucket, f\"configs/{config_name}.json\", tmp_path)\n        \n        file_fd = os.open(tmp_path, os.O_RDONLY | os.O_NOFOLLOW)\n        try:\n            file_stat = os.fstat(file_fd)\n            if file_stat.st_uid != os.getuid():\n                return False\n            if not stat.S_ISREG(file_stat.st_mode):\n                return False\n            \n            with os.fdopen(os.dup(file_fd), 'r') as f:\n                config_data = f.read()\n        finally:\n            os.close(file_fd)\n        \n        lambda_client = boto3.client('lambda')\n        lambda_client.update_function_configuration(\n            FunctionName=lambda_arn,\n            Environment={'Variables': {'CONFIG': config_data}}\n        )\n        return True\n    finally:\n        if tmp_path and os.path.exists(tmp_path):\n            os.unlink(tmp_path)\n        if fd is not None:\n            try:\n                os.close(fd[0])\n            except OSError:\n                pass"}