FlawOpen

flawopen.com/Безопасность памяти/strcpy vs strncpy

Безопаснее ли strncpy(), чем strcpy()?

CWE-787: запись за границами буфера; CWE-170: неправильное завершение нулёмСправочная страница
Краткий ответ

Поле фиксированной ширины похоже на ряд ячеек: последняя должна содержать маркер конца, если код будет считать байты C-строкой. strncpy() записывает не более n байт и предотвращает выход за буфер только если n не больше его реального размера. Если источник заполняет лимит, нулевой терминатор не добавляется. Следующая строковая функция может читать за пределами назначения в поисках отсутствующего маркера.

ОБЕ ФУНКЦИИ НЕПРАВИЛЬНЫ
char dst[16];

/* 1. Unbounded — classic overflow */
strcpy(dst, src);

/* 2. Bounded, but may not terminate.
   If strlen(src) >= 16, dst has no
   '\0' and every later strlen/printf
   reads out of bounds.            */
strncpy(dst, src, sizeof(dst));

/* 3. Off-by-one: room for 16 chars
   plus a terminator = 17 bytes,
   but dst only holds 16.          */
strncpy(dst, src, sizeof(dst));
dst[sizeof(dst)] = '\0';   /* OOB! */
ПРАВИЛЬНО
char dst[16];

/* Preferred: snprintf always
   null-terminates (when size > 0),
   and tells you about truncation. */
int n = snprintf(dst, sizeof(dst),
                 "%s", src);
if (n < 0 || (size_t)n >= sizeof(dst)) {
    /* truncated — handle it */
    return ERR_TOO_LONG;
}

/* Or strlcpy where available —
   always terminates, returns the
   length it tried to create.      */
if (strlcpy(dst, src, sizeof(dst))
        >= sizeof(dst)) {
    return ERR_TOO_LONG;
}

Для чего изначально предназначалась strncpy

Это поведение не является дефектом: strncpy() предназначена для полей фиксированной ширины с заполнением нулями, а не для безопасного копирования C-строк в общем случае. Если исходная строка короче n, функция дополняет назначение нулевыми байтами до n - 1. Если среди первых n байт источника нет нуля, копируются ровно n байт, а назначение не завершается. Это подходит некоторым фиксированным полям, но опасно для API, ожидающих строку с нулевым терминатором.

Если источник короче n, дополнение записывает всё поле целиком. Для большого буфера и короткой строки это может быть менее эффективно, чем копирование до терминатора.

Всегда проверяйте усечение

Предотвратить переполнение — только половина задачи. Тихое усечение тоже опасно: укороченный hostname, путь или значение авторизации может изменить смысл. snprintf возвращает длину, которую хотела бы записать; сравните её с размером буфера. Неверное прочтение этого значения как числа записанных байтов имело значение и в случае Citrix Bleed.

Частые вопросы

А что насчёт strcpy_s и функций Annex K?

Они завершают строку и проверяют границы, но их переносимость неодинакова: хорошая поддержка в Windows и неполная — в других системах. Для нескольких платформ snprintf обычно доступнее.

Входит ли strlcpy в стандарт C?

Нет. Она появилась в OpenBSD и доступна в BSD и macOS; её также добавили в новые версии glibc. Проверьте поддержку на целевых системах.

Какие функции использовать в C++?

Предпочитайте std::string и std::string_view, которые управляют длиной. Функции C используйте только на границе API, требующей сырой буфер.

Источники

См. также CWE и CVE: в чём разница? Mass assignment: риски и безопасные списки полей Утечка состояния serverless: изоляция данных запросов PHP == и сравнение magic hash Безопасен ли Rust с блоком unsafe?