Привет! Меня зовут Николай Никитас, я бэкенд-разработчик в Авито. В команде я занимаю роль securtity-чемпиона, то есть отвечаю за безопасность проекта.
Чтобы узнать, есть ли в программе уязвимости, мы используем статические анализаторы кода. Это бинарные файлы, которые считывают исходный код программы до сборки и запуска проекта. Так можно найти синтаксические ошибки, потенциальные баги, уязвимые места, бесконечные циклы.
Я расскажу про три статических анализатора, которые заточены под Go: GoSec, Go Vulnerability Manager, GoKart.
Анализатор GoSec
https://github.com/securego/gosec
Это популярный и самый старый из актуальных анализаторов безопасности для Go. В его базе 35 уязвимостей, которые встречаются чаще всего. Все опасности, которые они несут, описаны в стандарте CVE.
GoSec CI-интегрирован с платформами Github, Gitlab, Jenkins, Travis CI и другими. Проект поддерживает сообщество разработчиков Go.
GoSec распространяют в формате бинарного файла. Его нужно установить из репозитория Github командой go install
github.com/securego/gosec/v2/cmd/gosec@latest
После этого запустить командой gosec ./...
В Авито мы используем GoSec в составе golangci-lint. Проверки запускаются локально у каждого разработчика на этапе предсборки, до деплоя.
Анализатор Go Vulnerability Manager
https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck
Go Vulnerability Manager разработала команда Go Security Team. Она развивает язык Go и лучше всех знает о его уязвимостях. В базе анализатора больше 340 уязвимостей, каждая описана в стандартах CVE и CWE.
Go Vulnerability Manager умеет сканировать исходный код и уже собранные бинарные файлы. Он поможет узнать, насколько безопасны чужие программы.
Анализатор устанавливается из репозитория командой go install golang.org/x/vuln/cmd/govulncheck@latest
Запускается командой govulncheck ./...
Источники данных Go Vulnerability Manager — международные базы данных уязвимостей NVD и GHSA, open-source-пакеты уязвимостей Go, фиксы безопасности от Go Security.
Данные из всех источников попадают в единую базу Go Vulnerability Manager. Её можно интегрировать через VS Code или запустить бинарный файл сканера командой govulncheck
. Также лицензия позволяет без ограничений использовать исходники кода анализатора в своих проектах.
Анализатор GoKart
https://github.com/praetorian-inc/gokart
Наименее известный, но тоже хороший статический анализатор. Его создатели вдохновляются GoSec и хотят сделать ещё лучше. Они используют ту же базу данных из 30+ уязвимостей и похожий принцип работы.
Проектом занимается не только сообщество. Разработку GoKart поддерживает крупная компания Praetorian. Есть надежда, что это поможет ему стабильно развиваться.
GoKart устанавливается из бинарного файла командой go install github.com/praetorian-inc/gokart@latest
Команда для запуска — gokart scan .
Какие уязвимости находит GoSec
Общие уязвимости. В этом коде в комментариях подписаны уязвимые места, которые нашел GoSec:
func Float64bits(f float64) uint64 {
// G103 (CWE-242): Use of unsafe calls should be audited (Confidence: HIGH, Severity: LOW)
return *(*uint64)(unsafe.Pointer(&f))
}
func main() {
username := "admin"
// G101 (CWE-798): Potential hardcoded credentials (Confidence: LOW, Severity: HIGH)
var password = "Flag{keep_your_eyes_open)"
println("Doing something with: ", username, password)
// G12 (CWE-200): Binds to all network interfaces (Confidence: HIGH, Severity: MEDIUM)
// G104 (CWE-703): Errors unhandled. (Confidence: HIGH, Severity: LOW)
net.Listen("tcp", "@.0.0.0:80")
}
В третьей строке потенциально серьёзная уязвимость — пакет unsafe. Он позволяет внедрить в достаточно безопасный Go операции, которые работают с памятью напрямую, как в C/C++. Например, взять указатель на небезопасную область памяти и из неё перемещаться в другие области. В этом случае есть вероятность не уследить за границами переменных и получить огромную дыру в безопасности приложения.
В девятой строчке — одна из наиболее неприятных уязвимостей. Это хардкод credential, секретов и паролей. GoSec ищет переменные с названиями вроде password, pswd, login и другим похожим, которые содержат строку. После этого помечает её комментарием, чтобы разработчик обратил на нее внимание.
Но GoSec не умеет искать более сложные ошибки. Например, если пароль возвращает функция.
В 15-й строке — две уязвимости. Первая — попытка слушать 80 порт по TCP/IP 0.0.0.0. Это wildcard-интерфейс — потенциально опасное место, который разработчик скорее всего не планировал оставлять в коде. Вторая — нет обработчика для ошибки, которую вернёт функция net.listen.
Инъекции. GoSec ищет пять разных вариантов, я решил показать два:
// G201 (CWE-89): SQL string formatting (Confidence: HIGH, Severity: MEDIUM)
q := fat.Sprintf("SELECT * FROM foo where name = '&s'", os.Args[1])
if _, err := db.Query(q); err t= nil {
panic(err)
}
// G24 (CWE-78): Subprocess launched with a potential tainted input or cmd arguments (Confidence: HIGH, Severity: MEDIUM)
if err := syscall.Exec("rm", []string{"-rf", q}, nil); err != nil {
panic(err)
}
Первый вариант — это SQL-инъекции. Во второй строке есть попытка вставить в строку значение из аргументов с помощью функции Sprintf()
. Здесь не используются параметризированные запросы, а это потенциальная дыра в безопасности базы данных.
Второй вариант — вызов syscall,
в котором в качестве аргумента передаётся значение функции. В примере это rm
и rf
, в которых хранится значение из SQL-запроса. Уязвимость опасная: через инъекцию с использованием syscall
можно получить полный доступ к серверу или контейнеру с базой данных.
Уязвимости файловой системы. GoSec находит три вида:
// G306 (CWE-276): Expect WriteFile permissions to be 0600 or less (Confidence: HIGH, Severity: MEDIUM)
if err := os.WriteFile("some.txt", []byte("some text"), 0777); err != nil {
panic(err)
}
// G303 (CWE-377): File creation in shared tmp directory without using ioutil.Tempfile (Confidence: HIGH, Severity: MEDIUM)
if _, err := os.Create("/usr/tmp/123"); err != nil {
panic(err)
}
file := os.Getenv("SOME_FILE")
// G304 (CWE-22): Potential file inclusion via variable (Confidence: HIGH, Severity: MEDIUM)
if _, err := os.ReadFile("/tmp/" + file + "/blob"); err !=nil {
panic(err)
}
Во второй строке разработчик назначает уровень прав доступа к файлу. Программе нужно только записать данные в файл, но при этом стоит доступ 0777 — для всех пользователей. GoSec предлагает понизить уровень до 0600, то есть разрешить читать и записывать файл только его владельцу.
В седьмой строке пример другой потенциальной уязвимости — предсказуемая директория для временных файлов. В таких файлах может быть много чувствительной информации — например, персональные данные из лога. В Go для создания временных файлов есть специальные функции, которые генерируют непредсказуемый путь. Но если пытаться положить временный файл напрямую в директорию tmp, то возникнет ещё одна дыра в безопасности.
В 14 строке выполняется инъекция в путь для чтения файла, а не в базу. Функция ReadFile()
получает в виде аргумента путь, который состоит из конкатенации двух параметров и переменной окружения file.
Такая уязвимость позволяет злоумышленнику переопределить переменную окружения и получить доступ к другим файловым директориям.
Уязвимости криптографии. GoSec умеет находить ненадёжные конфигурации TLS и ключи RSA:
// G402 (CWE-295): TLS MinVersion too low. (Confidence: HIGH, Severity: HIGH)
cfg c= tls.Config{}
// G402 (CWE-295): TLS InsecureSkipVerify set true. (Confidence: HIGH, Severity: HIGH)
cfg = tls.Config{
InsecureSkipVerify: true,
}
println(cfg)
// G403 (CWE-310): RSA keys should be at least 2048 bits (Confidence: HIGH, Severity: MEDIUM)
if _, err := rsa.GenerateKey(bytes.NewReader([]byte{}), 1024); err != nil {
panic(err)
}
// G404 (CWE-338): Use of weak random number gene: (math/rand instead of crypto/rand) (Confidence: MEDIUM, Severity: HIGH)
println( rand.Int63())
Во второй строчке ошибка в том, что в коде не объявлены минимальная и максимальная версии TLS. Это значит, что программа может использовать минимальную из всех доступных версий — она уже устарела и содержит уязвимости.
В шестой строчке напрямую указано, что нужно сбросить верификацию. GoSec обращает внимание разработчика на это, так как это может быть потенциальной ошибкой.
В 11-й строке задаётся длина ключа RSA. GoSec указывает, что для корректной работы ключа его длина должна быть минимум 2048 бит.
В 17-й строчке используется небезопасная функция-рандомайзер. В Go их две: из пакета математики и криптографическая. В математической функции rand.Int63()
применяются предсказуемые алгоритмы, которые легко взломать. Поэтому GoSec проверяет, какой именно рандомайзер указан в конкретном коде и подсказывает, если его нужно заменить.
Устаревшие технологии. GoSec находит четыре устаревших способа хэширования и сетевой пакет с хэшем внутри:
// G501 (CWE-327): Blocklisted import crypto/md5: weak cryptographic primitive (Confidence: HIGH, Severity: MEDIUM)
_ = md5.New()
// G502 (CWE-327): Blocklisted import crypto/des: weak cryptographic primitive (Confidence: HIGH, Severity: MEDIUM)
_, _ = des.NewCipher( []byte{})
// G503 (CWE-327): Blocklisted import crypto/rc4: weak cryptographic primitive(Confidence: HIGH, Severity: MEDIUM)
_, _ = rc4.NewCipher([]byte{})
// G504 (CWE-327): Blocklisted import net/http/cgi: Go versions < 1.6.3 are vulnerable to Httpoxy attack: (CVE-2016-5386) (Confidence: HIGH, Severity: MEDIUM)
_ = cgi.Serve(http.DefaultServeMux)
// G505 (CWE-327): Blocklisted import crypto/shal: weak cryptographic primitive(Confidence: HIGH, Severity: MEDIUM)
_ = shal.New()
В этих протоколах для хеширования множество коллизий, поэтому они небезопасные. GoSec указывает, что их лучше не использовать.
Небезопасные ссылки. В Go есть особенность: итератор на каждом шаге цикла перезаписывает сам себя, но при этом не является ссылкой или отдельным значением. GoSec находит небезопасные ссылки на итераторы, которые могут сломать программу.
func bar(s *string) {
println(*s)
}
func main() {
// G601 (CWE-118): Implicit memory aliasing in for loop. (Confidence: MEDIUM, Severity: MEDIUM)
for _, v := range []string{"fool", "bar1"} {
bar(&v)
}
}
На восьмой строчке разработчик берёт указатель на итератор в цикле, то есть на переменную v. Из-за этого он будет получать не все значения v, а только несколько последних.
Если итераций будет 10 000, то программа выведет 150-200 из них. Так происходит, потому что итератор обновляется быстрее, чем выполняется функция println()
.
Результаты проверки на реальных проектах
Я выбрал три проекта, чтобы на их примере показать работу анализаторов кода. Для проверки я запускал анализаторы в два действия: скачивал репозиторий и запускал.
Kubernetes. Весь исходный код проекта есть в открытом доступе. Из трёх анализаторов справился только GoSec, который нашел почти 5000 уязвимостей. Из них 156 — высокого уровня.
Go Vulnerability Manager и GoKart не смогли выполнить анализ, потому что Kubernetes не использует модули go.mod. Я попробовал инициализировать модуль и нашёл пакеты, в которых нет обязательных синтаксических конструкций. То есть, для сборки кроме go build они используют замены исходников и шаблоны.
Docker CE. Старый движок Docker тоже смог проанализировать только GoSec. Он нашел почти 600 уязвимостей, из которых 12 — высокого уровня. Go Vulnerability Manager и GoKart снова не справились.
Мой рабочий проект. Здесь запустились все три анализатора. GoKart при этом не нашел ничего, а Go Vulnerability Manager — две уязвимости в стандартной библиотеке Go. В свежей версии языка эти уязвимости уже поправили.
GoSec выявил девять уязвимостей, из них две были с высоким уровнем. Но на самом деле обе они связаны с переменной password, в которой не были записаны пароли. Семь среднеуровневых уязвимостей — это игнорирование ошибок.
Статические анализаторы кода действительно помогают найти уязвимости. Это показывает пример даже таких крупных проектов, как Kubernetes и Docker CE. При этом использовать анализаторы не сложно, для этого достаточно знать пару команд.
Я не советую воспринимать эти инструменты как гарантию поиска всех уязвимостей, но они точно покажут, где нужно быть внимательнее к коду. Результаты анализа могут быть ложноположительные, как в моём примере с переменной password. Но могут быть и ложноотрицательные, если уязвимость ещё не появилась в базах данных сканеров.
И главное, что нужно помнить: статический анализатор кода не спасёт от ошибок в бизнес-логике или намеренного внедрения бэкдора. В этом случае поможет только качественное ревью кода.
Предыдущая статья: Как я очень захотел перейти в бэкенд из фронтенда — и перешёл
Комментарии (5)
siberianlaika
16.06.2023 09:21Судя по статье gosec выглядит наиболее рабочим вариантом, в отличие от gokart и govulncheck. У себя применяем golangci-lint с gosec как часть CI.
Kolya59 Автор
16.06.2023 09:21Да, он наиболее стабильный. govulncheck, думаю, еще раскачается со временем, а с go-kart не ясно(
HunterXXI
как я понял это pre-commit проверки в самих IDE разработчиков. Встраиваете ли вы эти же проверки в пайплайн на случай если разработчик игнорирует сообщения об уязвимостях?
Статистические анализаторы грешат ложными срабатываниями (по крайней мере на уязвимости из баз cve) можно ли в эти инструменты добавлять исключения для определённых проверок?
Kolya59 Автор
1) Да, встраиваем, все проверки линтерами проводятся как локально, так и на CI, блокируя деплой для уязвимого кода
2) Да, можно через `nolint` директивы, либо через конфигурацию запускаемых линтеров
ntoskernel
Привет! На связи команда продуктовой безопасности Авито)
Проверки у нас встроены параллельно CI-пайплайну: сканируем каждый пуш в любой репозиторий, дальше отслеживаем дедуплицированные файндинги по их жизненному циклу (переходы между ветками, исчезновения, пометки как false positive и т.д.). Nolint, разумеется, игнорируем, фолзами промечаем конкретные находки либо сами, либо по просьбе разработчиков, которых уведомляем алертами и задачами.
Для сервисов на go последние 4 года используем gosec, последние 2 - codeql в пару к нему. Пуши в репозитории не блокируем из-за находок по языковым сканерам, блокируем только вычурно уязвимые зависимости и секреты.