Вопрос #1614930

Разберите проблему TOCTOU в POSIX при проверке прав доступа и последующем открытии файла; приведите способы устранения этой гонки и примеры API, которые помогают её избежать

Ответы
1


Проблема TOCTOU (Time-Of-Check — Time-Of-Use) в POSIX: кратко
- Суть: между проверкой условий доступа и фактическим использованием объекта файловой системы (открытие, изменение) может произойти изменение этого объекта (перемена симлинка, замена файла, изменение прав) — атакующий использует это «окно» для подмены объекта и обхода проверок.
- Типичный небезопасный паттерн: сначала вызывают access(\(2\)) для проверки прав, затем open(\(2\)). Между этими вызовами путь может измениться, поэтому результат проверки не гарантирует безопасность открытия.
Почему access() опасен
- access(\(2\)) проверяет доступ к пути отдельно от последующего использования: результат может устареть.
- Для setuid/setgid программ отдельный недостаток — проверка делается в контексте реального или эффективного UID, что ещё больше усложняет логику.
Способы устранения гонки TOCTOU и соответствующие API / паттерны
1) Не проверять отдельно — просто попытаться открыть и обработать ошибку
- Вместо access + open делайте просто open(path, flags). Если прав недостаточно, open вернёт ошибку (EACCES). Это убирает окно между проверкой и использованием.
2) Работать с файловым дескриптором (fd) вместо имени
- Открыли файл — дальше используйте операции по fd: fstat(\(2\)), fchmod(\(2\)), fchown(\(2\)), ftruncate(\(2\)), read/write по fd. Операции по fd привязаны к конкретному inode и атомарны относительно подмены имени.
- Пример-паттерн:
- fd = open(path, O_RDONLY | O_NOFOLLOW);
- if (fd < 0) — обработать;
- fstat(fd, &st) — проверить S_ISREG(st.st_mode) и st_uid и т.д.
3) Открывать файл относительно дескриптора директории (openat) и запрещать следование симлинкам
- Откройте родительскую директорию безопасно: dirfd = open(parent, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
- Затем используйте openat(dirfd, name, flags | O_NOFOLLOW). Это снижает вероятность атаки через смену текущего каталога или через промежуточные операции.
- API: openat(\(2\)), O_NOFOLLOW, O_DIRECTORY, O_CLOEXEC.
4) Атомарные операции создания/замены
- Для создания нового файла только если он ещё не существует: open(path, O_CREAT | O_EXCL, mode) — атомарно.
- Для безопасного временного файла: mkstemp(\(3\)) / mkostemp(\(3\)), либо Linux O_TMPFILE с последующим linkat(\(2\)) / rename(\(2\)).
- Для замены существующего файла используйте rename(\(2\)) — атомарная замена.
5) Проверка свойств уже открытого fd
- Если нужно убедиться, что открытый файл — тот самый (не симлинк, нужный владелец, режим), делайте fstat(fd) и сравнивайте с ожидаемым (S_ISREG, st_uid, st_mode). Это безопаснее, чем stat(path) — stat по имени подвержен TOCTOU.
6) Использование флагов и вариантов проверки доступа
- faccessat(\(2\)) с флагом AT_EACCESS проверяет доступ в контексте эффективных UID/GID; это уменьшает путаницу с real/effective UID, но не решает полностью TOCTOU (всё ещё отдельный чек).
- Для предотвращения следования симлинков используйте fstatat(\(2\)) с AT_SYMLINK_NOFOLLOW или флаг O_NOFOLLOW при open.
7) Минимизировать привилегии и переключаться надёжно
- Для setuid-программ: либо временно сбросьте привилегии (seteuid) перед операциями с пользовательскими путями, либо выполните нужные операции с учётом эффектов real/effective UID таким образом, чтобы не было необходимости в отдельной предварительной проверке.
- В целом правило: не поднимайте привилегии только для того, чтобы выполнить проверку; если необходимо, выполнить проверку/открытие уже под минимальными правами.
8) Специальные API и расширенные возможности (Linux/нередко специфичные)
- O_TMPFILE (создание анонимного временного файла).
- name_to_handle_at / open_by_handle_at — работа с дескрипторами объектов файловой системы (специфично, требует поддержки FS и прав).
- unlinkat(\(2\), renameat(\(2\)) — для атомарных манипуляций с именами.
Краткие примеры безопасных паттернов
- Простой: попытаться открыть
- fd = open(path, O_RDONLY);
- if (fd < 0) — проверка errno (EACCES и т.д.).
- Через директорию + запрет симлинков:
- dirfd = open(parent, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
- fd = openat(dirfd, name, O_RDWR | O_NOFOLLOW);
- if (fd >= 0) { fstat(fd, &st); /* проверить st */ }
- Создание временного файла безопасно:
- fd = mkstemp(template); // или mkostemp
- /* работать по fd, затем rename для окончательной атомарной установки имени */
Заключение (рекомендации)
- Избегайте паттерна access(\(2\)) → open(\(2\)). Открывайте и проверяйте уже открытый fd.
- Используйте openat(\(2\)) + O_NOFOLLOW + открытие родительской директории, fstat по fd и флаги типа O_EXCL / O_TMPFILE / mkstemp для атомарных операций.
- Сбрасывайте/минимизируйте привилегии там, где возможно.
Если нужно, могу привести короткие фрагменты кода на C для конкретного паттерна (openat + fstat) или для безопасного создания временного файла.

Минимум 10 символов
Прямой эфир