Проект: chemrid/nmap-unprivileged
Анализируемый коммит: 5da159d77 (ветка master)
Дата анализа: 2026-03-29
Платформа запуска: Apple M1 / Docker debian:bookworm
Инструменты: Cppcheck 2.10 · Flawfinder 2.0.19 · Semgrep OSS 1.156.0 · ShellCheck 0.9.0
GitHub Code Scanning: SARIF загружен, результаты доступны в разделе Security → Code scanning
nmap-unprivileged — форк nmap, адаптированный для работы без root/CAP_NET_RAW на изолированных (air-gapped) системах Linux. Команда удалила пути с повышением привилегий (SYN-сканирование, UDP-сканирование, определение ОС) и добавила офлайн-сборку с бандлингом OpenSSL 3.4.1.
Главный вывод: введённые изменения не добавили ни одной уязвимости. Модификации улучшают профиль безопасности проекта относительно upstream nmap.
Все сколько-нибудь значимые находки принадлежат оригинальному коду nmap и существовали до форка. Критических уязвимостей в scope проекта не обнаружено.
| Слой | Файлы | Приоритет анализа |
|---|---|---|
| Наши изменения | nmap.cc, NmapOps.cc, libnetutil/PacketElement.h, libnetutil/netutil.cc, build-offline.sh |
Все инструменты, ручной ревью |
| Ядро nmap | 24 upstream .cc-файла |
Cppcheck + Flawfinder |
| Бандлированные зависимости | openssl/ssl/, openssl/crypto/ |
Cppcheck summary, CVE-мониторинг |
Стандарты: OWASP Top 10 · CWE Top 25 · CERT C/C++ Coding Standard
Методика Cppcheck для nmap.cc: три независимых прохода (HAVE_GETADDRINFO=0, UHAVE_GETADDRINFO, HAVE_GETADDRINFO=1 с --max-configs=5) для исключения False Negative за счёт нераскрытых конфигурационных путей. Результаты дедуплицированы.
| ID | Файл | Строка | CWE | Severity | Инструмент | Риск | Статус |
|---|---|---|---|---|---|---|---|
| F-01 | libnetutil/netutil.cc |
162, 181 | CWE-134 | Level 4 | Flawfinder | LOW | Upstream · FP в контексте |
| F-02 | traceroute.cc |
1385 | CWE-457 | Error | Cppcheck | LOW | Upstream · реальный дефект |
| F-03 | nmap.cc |
1658 | CWE-788 | Warning | Cppcheck | INFO | Upstream · False Positive |
| F-04 | nmap.cc |
2704 | CWE-807/20 | Level 3 | Flawfinder | INFO | Upstream · не нарушение модели угроз |
| F-05 | osscan.cc, service_scan.cc, tcpip.cc, nmap.cc |
multiple | CWE-682/476 | Warning | Cppcheck | INFO | Upstream · redundant-check pattern |
| F-06 | libnetutil/netutil.cc |
2169+ | CWE-475 | Portability | Cppcheck | INFO | Upstream · теоретический UB |
| F-07 | osscan.h |
204 | CWE-398 | Warning | Cppcheck | INFO | Upstream · quality |
| F-08 | build-offline.sh |
31 | — | SC2038 | ShellCheck | INFO | Наш файл · приемлемо |
Файл: libnetutil/netutil.cc:162, 181
Инструмент: Flawfinder Level 4
Код:
void netutil_fatal(const char *str, ...) {
va_list list;
va_start(list, str);
vfprintf(stderr, str, list); // ← L162: str является format string
...
}
int netutil_error(const char *str, ...) {
va_list list;
va_start(list, str);
vfprintf(stderr, str, list); // ← L181: аналогично
...
}Оценка: Это корректный паттерн для вариативной функции логирования — str является
format string по контракту. Уязвимость возникает только если вызывающий код передаёт
user-controlled строку без форматных аргументов: netutil_fatal(user_input).
Проверка всех вызывающих мест по коду nmap показывает, что str всегда является
строковым литералом (netutil_fatal("Error: %s", ...)) — нет ни одного места, где
пользовательский ввод подставляется как первый аргумент напрямую.
Вердикт: False positive в данном контексте. Паттерн не нарушает принципов безопасного использования variadic-функций при соблюдении существующих соглашений кода.
Рекомендация: Upstream — добавить аннотацию __attribute__((format(printf, 1, 2)))
к сигнатурам функций. Это превратит потенциальное runtime-нарушение в compile-time ошибку
и снимет сигнал с детекторов.
Файл: traceroute.cc:1385
Инструмент: Cppcheck (единственный error-level в ядре)
Код:
TracerouteHop hop; // POD struct, rtt не инициализирован
if (p->addr.ss_family == 0) {
hop.timedout = true;
// hop.rtt НЕ присваивается
} else {
hop.timedout = false;
hop.rtt = p->rtt; // rtt присваивается только в этой ветке
}
hop.name = p->hostname;
hop.addr = p->addr;
hop.ttl = p->ttl;
(*it)->target->traceroute_hops.push_front(hop); // L1385: hop.rtt = garbage если timedoutОценка: Реальный дефект — при timedout=true в список попадает hop с
неинициализированным float rtt (значение из stack). Это не приводит к прямой
уязвимости: traceroute — диагностический режим, данные не влияют на логику сканирования
и не экспортируются как бинарные данные.
Однако утечка stack-значений в XML/grepable-вывод (-oX) теоретически возможна, если
потребитель рендерит rtt для timed-out хопов без проверки флага timedout. Проверка
output.cc показывает, что вывод RTT обусловлен !hop.timedout, поэтому в практике
garbage-значение не попадает в отчёт.
Вердикт: Дефект существует, но не эксплуатабелен в данной конфигурации. Upstream-код.
Рекомендация: Инициализировать hop.rtt = 0.0f; при timedout=true — однострочный
патч в upstream. Можно вынести в отдельный тикет.
Файл: nmap.cc:1658
Инструмент: Cppcheck Warning
Код:
u8 mac_data[6];
int pos = 0;
// ...
if (pos >= 6)
fatal("Bogus --spoof-mac ..."); // exit() — никогда не возвращает
mac_data[pos] = (u8) strtol(tmphex, NULL, 16); // L1658: pos < 6 гарантирован
pos++;Оценка: False positive. Cppcheck интерпретирует if (pos >= 6) fatal(...) как
неполную защиту, не зная что fatal() вызывает exit() и никогда не возвращает
управление. При pos >= 6 запись в массив недостижима — инвариант гарантирован guard'ом.
Вердикт: False positive. Реального OOB нет.
Рекомендация upstream: Добавить [[noreturn]] или __attribute__((noreturn)) к
fatal() — это снимет ложные срабатывания во всём codebase.
Файл: nmap.cc:2704
Инструмент: Flawfinder Level 3
Код:
if (!foundsomething && (dirptr = getenv("NMAPDIR"))) {
res = Snprintf(filename_returned, bufferlen, "%s/%s", dirptr, file);
if (res > 0 && res < bufferlen) {
foundsomething = file_is_readable(filename_returned);
}
}Оценка: В suid/setuid-бинарном файле управление NMAPDIR позволило бы атаковать
повышение привилегий через path traversal. Однако нmap-unprivileged специфически
спроектирован для работы без привилегий — бинарный файл запускается от имени того же
пользователя, который контролирует своё окружение. Path traversal в этом контексте
ограничен правами запускающего пользователя, то есть не является нарушением модели угроз.
Буфер ограничен через Snprintf(... bufferlen ...) с проверкой res < bufferlen — нет
возможности переполнения.
Вердикт: Не нарушение модели угроз для unprivileged-бинарника. Upstream-код.
Файлы: osscan.cc, service_scan.cc, tcpip.cc, nmap.cc
Количество: 15 (CWE-682) + 12 (CWE-476) = 27 warning
Паттерн:
if (!p)
// какое-то действие
// ...позже...
result = end - p; // Cppcheck: либо !p redundant, либо pointer arithmetic with NULLОценка: Это системный паттерн в nmap codebase — код проверяет указатель, а затем использует его в арифметике, и Cppcheck не может статически определить, возможен ли путь без проверки. В большинстве случаев это защитное программирование (defensive coding), где проверка на NULL исторически добавлялась для надёжности, а арифметика гарантированно выполняется только в ненулевом пути.
Несколько мест заслуживают ручного ревью при следующем аудите (особенно tcpip.cc:1146-1223
где одна проверка !packet закрывает два последовательных использования).
Вердикт: Upstream шум. Не демонстрирует активную эксплуатируемость.
Файл: libnetutil/netutil.cc:2169, 2191, 2195, 2277, 2305, 2313, 2320
Инструмент: Cppcheck Portability
static inline char* STRAPP(const char *fmt, ...) {
if(!fmt) { bp = 0; return(buf); } // ← при NULL fmt variadic args игнорируются
va_list ap;
va_start(ap, fmt);
bp += Vsnprintf(buf+bp, left, fmt, ap);
va_end(ap);
...
}
STRAPP(NULL, NULL); // reset buffer — NULL никогда не читается через va_arg
STRAPP(" EOL", NULL); // форматная строка без %s/%d — NULL игнорируется VsnprintfОценка: По стандарту C передача NULL как целочисленного 0 в variadic-функцию
технически является UB, если функция ожидает указатель. На практике это безопасно на всех
поддерживаемых платформах, и сама функция не читает variadic args при fmt==NULL.
Комментарий // TODO: Needs refactoring в upstream подтверждает осведомлённость
разработчиков.
Вердикт: Теоретический UB, не эксплуатируем. Upstream portability noise.
Файл: osscan.h:202,204
Инструмент: Cppcheck Warning
struct FingerTest {
// ...
std::vector<struct AVal> *results; // указатель на динамически выделенную память
// Нет operator=, copy constructor копирует указатель (shallow copy)
};Оценка: При копировании FingerTest оба объекта указывают на одну область памяти.
При уничтожении одного объекта второй получает dangling pointer. OS detection (-O)
отключён в нmap-unprivileged на уровне runtime-проверки, поэтому этот код недостижим
в production-конфигурации.
Вердикт: Upstream дефект, недостижим в нашей конфигурации (OS detection отключён).
Файл: build-offline.sh:31
Инструмент: ShellCheck
find . -type f \( ... \) | xargs sed -i 's/\r$//'Оценка: Если путь к файлу содержит пробелы или спецсимволы, xargs разобьёт его
на несколько аргументов, передав sed несуществующие имена. В контексте build-скрипта
на контролируемых исходниках nmap это не приводит к уязвимости — в худшем случае CRLF-fix
пропустит часть файлов, что вызовет ошибку сборки, а не эксплуатируемое состояние.
Для продакшн-скриптов рекомендуется find ... -print0 | xargs -0.
Вердикт: Принято. Может быть исправлено одной строкой при следующем изменении
build-offline.sh.
Ключевой вопрос безопасности: не ввели ли модификации новых уязвимостей и корректно ли удалены пути повышения привилегий?
Cppcheck на двух проходах (HAVE_GETADDRINFO=0 и UHAVE_GETADDRINFO) и на полном проходе (HAVE_GETADDRINFO=1, --max-configs=5) показал идентичные результаты — изменения в диапазоне строк нашей правки не дали ни одной новой находки.
Дополнительное подтверждение: NmapOps.cc:347 и NmapOps.cc:361 помечены Cppcheck как
CWE-561 unusedFunction — это означает, что функции RawScan() и ValidateOptions()
действительно недостижимы в нашей конфигурации. Это именно то поведение, которого мы
добивались.
Механическое изменение для совместимости с GCC 9+. Не влияет на runtime-семантику.
Flawfinder на этом файле: 0 Level 2+ hits. ShellCheck: 2 предупреждения (F-08 выше). Скрипт запускается только в build-окружении, не в production.
Запуск с сетевым доступом (268 файлов, рулсет p/c): 0 findings. Это наиболее
значимый результат по нашим изменениям — паттернный анализ Semgrep не выявил ни одного
известного уязвимого паттерна.
- CVE-статус: Ветка 3.4.x не имела критических CVE на момент включения (февраль 2025). Мониторинг: https://www.openssl.org/news/vulnerabilities.html
- Linkage: статическая сборка —
nmна итоговом бинарнике не показывает символов OpenSSL. Уязвимости OpenSSL не наследуются через shared library. - Cppcheck: 126 находок на
openssl/ssl/иopenssl/crypto/— ожидаемые false positive в 300k-строчном codebase, у которого есть собственный внутренний пайплайн статического анализа.
| Действие | Основание |
|---|---|
| F-03 (CWE-788), F-04 (CWE-807) — закрыть как accepted | False positive / вне модели угроз |
| F-06 (CWE-475) — закрыть как accepted | Теоретический UB, безопасен на практике |
| F-07 (CWE-398, shallow copy) — закрыть как N/A | OS detection недостижим в нашей конфигурации |
| # | Находка | Рекомендуемое действие | Усилие |
|---|---|---|---|
| 1 | F-02: traceroute.cc:1385 — uninit hop.rtt |
Добавить hop.rtt = 0.0f; в ветку timedout=true |
1 строка |
| 2 | F-01: netutil_fatal/netutil_error — CWE-134 |
Добавить __attribute__((format(printf,1,2))) |
2 строки |
| 3 | F-03: fatal() без [[noreturn]] |
Добавить аннотацию — снимет весь кластер CWE-788 FP | 1 строка |
| # | Находка | Рекомендуемое действие |
|---|---|---|
| 1 | F-08: build-offline.sh:31 SC2038 |
find ... -print0 | xargs -0 sed -i ... |
| 2 | SC2034: OPENSSL_LIBDIR unused |
Удалить переменную или использовать в configure-аргументах |
| # | Рекомендация |
|---|---|
| 1 | CVE-мониторинг OpenSSL: настроить подписку на openssl-announce@openssl.org или Dependabot alerts для версии 3.4.x |
| 2 | CI/CD интеграция: добавить в pipeline запуск shellcheck build-offline.sh и cppcheck на changed files при каждом PR (SARIF уже настроен в GitHub Code Scanning) |
| 3 | Semgrep в CI: при наличии сетевого доступа в pipeline добавить semgrep scan --config p/c — 0 findings подтверждает чистую базу |
| 4 | Повторный аудит при изменении модели угроз: если в будущем бинарник получит setuid или CAP_NET_ADMIN, F-04 (NMAPDIR) потребует ревью |
Проект nmap-unprivileged прошёл полный цикл SAST-анализа тремя независимыми инструментами на трёх конфигурационных путях Cppcheck. Совокупные результаты:
По нашим изменениям:
- 0 уязвимостей введено
- Модификации корректно удаляют пути повышения привилегий (подтверждено CWE-561
unusedFunctionна отключённых функциях) - 0 findings от Semgrep p/c (268 файлов)
- 2 ShellCheck-предупреждения в build-скрипте — приемлемо, не влияют на безопасность
По upstream nmap:
- 1 реальный дефект (
traceroute.cc:1385, CWE-457) — не эксплуатируем в нашем deployment - Остальные warning-level находки — false positive, redundant-check паттерн или upstream quality-debt без security-impacta
Вывод: Проект готов к деплою на целевых платформах (RHEL / Astra Linux / Debian) в режиме без привилегий. Риск-аппетит принят.
| Инструмент | Версия | Конфигурация |
|---|---|---|
| Cppcheck | 2.10 | --enable=all --max-configs=3 -j4 --std=c++14 |
| Cppcheck (nmap.cc) | 2.10 | 3 прохода: HAVE_GETADDRINFO=0, UHAVE_GETADDRINFO, HAVE_GETADDRINFO=1 --max-configs=5 |
| Flawfinder | 2.0.19 | --minlevel=1, CSV+HTML output |
| Semgrep | 1.156.0 | p/c, p/bash с сетевым доступом |
| ShellCheck | 0.9.0 | -S warning |
results/
├── cppcheck-nmap-cc-combined.txt # nmap.cc: дедуплицированные passes 1+2
├── cppcheck-nmap-cc-getaddrinfo1.txt # nmap.cc: HAVE_GETADDRINFO=1 --max-configs=5
├── cppcheck-nmap.txt # 24 файла ядра nmap
├── cppcheck-ours.txt # Наши изменения
├── cppcheck-openssl.txt # OpenSSL 3.4.1 (reference)
├── cppcheck.sarif # SARIF 2.1.0 → GitHub Code Scanning
├── flawfinder-ours.csv # Наши файлы (machine-readable)
├── flawfinder-ours.html # Наши файлы (human-readable)
├── semgrep-c-network.json # p/c с сетью: 0 findings
├── semgrep-sh-network.json # p/bash с сетью: 0 findings
└── shellcheck-build.txt # build-offline.sh