Список рассылки

Начнем с самого очевидного места — списка рассылки Go. Нужно подписаться, чтобы получать всю важную информацию о безопасности прямо из источника. Обо всех релизах, содержащих исправления безопасности, сообщается в списке [email protected]. Подписавшись на рассылку, мы можем быть уверены, что не пропустим ни одного важного объявления.

Актуальная версия Go

Второй шаг — поддерживать актуальность версий Go в проектах. Несмотря на то, что мы не используем новейшие функции языка, обновление версии Go дает нам все исправления обнаруженных уязвимостей. Кроме того, новая версия Go предоставляет совместимость с новыми зависимостями. Она защищает приложения от потенциальных проблем с интеграцией.

Третий шаг — узнать, какие проблемы безопасности и CVE рассматриваются в релизах Go. Проверить это можно на веб-сайте истории релизов Go, а затем обновить язык до последней версии в файлах проектов go.mod.

После обновления до новых версий Go мы должны убедиться, что эта операция не создаст проблем совместимости и зависимостей, особенно со сторонними пакетами. Обновление может быть более рискованно, когда мы работаем над большими проектами с десятками или сотнями прямых и косвенных зависимостей пакетов.

Цель состоит в том, чтобы снижать риски, устраняя потенциальные проблемы зависимости. Проблемой может оказаться срочная необходимость рефакторинга существующего кода, чтобы он работал с новой зависимостью. Примеры таких проблем — изменение пакетов, API или сигнатур функций.

Зная, что мы используем версию Go без проблем с безопасностью, можно сконцентрироваться на исходном коде проекта, оценивать качество и безопасность кода при помощи статических анализаторов.

vet

Перед установкой и применением сторонних анализаторов рекомендуется выполнить нативную команду Go go vet. Ее мы можем использовать для анализа кода. Без аргументов она запускает инструмент со всеми параметрами, разрешенными по умолчанию. Инструмент сканирует исходный код и сообщает о потенциальных проблемах. Эти проблемы включают синтаксические ошибки в коде и некоторые программные конструкции, способные вызвать проблемы во время выполнения.

Наиболее распространенные проблемы включают ошибки горутин, неиспользуемые переменные и недостижимые области кода. Основное преимущество команды go vet заключается в том, что она — часть набора инструментов Go. Подробную документацию и примеры можно найти на веб-сайте, посвященном go vet.

staticcheck

Staticcheck — еще один статический анализатор кода. Это сторонний линтер, который помогает находить ошибки и выявляет возможные проблемы производительности. Он также объясняет обнаруженные проблемы, обеспечивает соблюдение стиля языка и предлагает упрощения и исправления кода с примерами. Помимо запуска staticcheck в конвейере CI, мы можем установить staticcheck на ноутбук как отдельный бинарный файл и сканировать код локально.

Установим последнюю версию:

go install honnef.co/go/tools/cmd/staticcheck@latest

В терминале нет ошибок? Если да, то мы готовы запустить сканирование. Но сначала проверим установленную версию, чтобы убедиться, что все в порядке:

staticcheck --version staticcheck 2024.1.1 (0.5.1)

Как и в случае с go vet, запуск staticcheck без аргументов по умолчанию вызывает все анализаторы кода. Этот подход прекрасно сочетается с философией программирования UNIX. Она заключается в использовании разумных значений по умолчанию и отсутствии необходимости заставлять пользователей выполнять ненужную «бумажную» работу.

Посмотрим, что этот инструмент может найти в репозитории NGINX Agent на GitHub.

Сначала нужно его клонировать:

git clone [email protected]:nginx/agent.git

Затем можно запустить его из корневого каталога проекта:

➜ agent git:(main) ✗ staticcheck ./...

Через некоторое время мы готовы проверить результаты сканирования. Перечисленные примеры можно разделить на три группы:

  • пакеты, методы или функции, которые устарели:
...
src/core/metrics/sources/cpu.go:111:9: times.Total is deprecated: Total returns the total number of seconds in a CPUTimesStat Please do not use this internal function. (SA1019)
...
test/component/nginx-app-protect/monitoring/monitoring_test.go:15:8: "github.com/golang/protobuf/jsonpb" is deprecated: Use the "google.golang.org/protobuf/encoding/protojson" package instead. (SA1019)
  • неиспользуемые переменные и поля:
src/core/metrics/sources/nginx_plus.go:74:2: field endpoints is unused (U1000)
src/core/metrics/sources/nginx_plus.go:75:2: field streamEndpoints is unused (U1000)
src/core/metrics/sources/nginx_plus_test.go:94:2: var availableZones is unused (U1000)
  • возможные проблемы, связанные с качеством кода:
src/core/nginx.go:791:4: ineffective break statement. Did you mean to break out of the outer loop? (SA4011)

Теперь мы готовы приступить к анализу выделенных проблем. Подробное глубокое погружение в кодовую базу выходит за рамки этой вводной статьи.

Обратим внимание на веб-сайты CWE, которые содержат массу информации о перечисленных слабых сторонах, чтобы изучить их позже:

  • Неиспользуемые переменные — CWE-563.
  • Использование устаревших конструкций — CWE-477.

golangci-lint

Третий анализатор, который мы собираемся использовать, — golangci-lint. Как и все инструменты Go, установить его можно разными способами, включая команду go install:

go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

Проверим, прошла ли установка, и посмотрим версию инструмента:

golangci-lint --version
golangci-lint has version v1.61.0 built with go1.23.2
...

Все выглядит хорошо.

Следуя правилу наименьшей неожиданности, golangci-lint запускает все линтеры, если мы вызываем его без аргументов.

Правило наименьшей неожиданности: при проектировании интерфейса всегда делайте наименее неожиданные вещи.

Что произойдет, если проверить клонированный ранее репозиторий agent? Покажет ли golangci-lint те же предупреждения и предложения? Выясним это.

Как и раньше, начнем сканирование проекта с его корневой директории:

➜ agent git:(main) ✗ golangci-lint run ./...

Практически сразу заметен список предложений, как улучшить код! Например:

src/extensions/nginx-app-protect/monitoring/processor/nap_test.go:60:14: S1025: the argument is already a string, there's no need to use fmt.Sprintf (gosimple)
 logEntry: fmt.Sprintf(`%s`, func() string {
 ^
src/plugins/common.go:85:5: S1009: should omit nil check; len() for []string is defined as zero (gosimple)
 if loadedConfig.Extensions != nil && len(loadedConfig.Extensions) > 0 {
    ^

Линтер указывает на конкретные файлы и строки, требующие нашего внимания. Задача сейчас — оценить код, внести изменения, запустить линтер второй раз и выполнить все модульные тесты. Если тесты зеленые, можно закоммитить обновленный код, работа сделана! Нужно запушить его на удаленный репозиторий.

Обнаружение состояний гонки

Состояния гонки в наших программах и библиотеках могут возникать, когда доступ к ресурсу одновременно пытаются получить несколько горутин. Эти состояния обнаруживаются, когда хотя бы одна горутина пытается записать (изменить) ресурс. Ресурс может быть глобальной переменной уровня пакета, действующей как счетчик. Такая ситуация в программе может привести к неуловимым ошибкам, которые трудно диагностировать и обнаружить.

Go имеет встроенную поддержку тестирования на состояний гонки. Эти тесты запускаются с помощью инструмента Go test с аргументом -race. Строка запускает детектор гонок и помогает выявить проблемы в программах с конкурентностью.

go test -race

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

Когда мы говорим Go: «Запусти тесты с аргументом -race», он компилирует код с включенным детектором гонок. Затем запускаются тесты и во время их выполнения проверяются возможные состояния гонки. При обнаружении гонок go test -race выведет подробный отчет. Он покажет, какие горутины и к каким ресурсам пытаются получить доступ.

Еще один способ повысить вероятность обнаружения проблем конкурентности — выполнять тесты параллельно. Для этого нужно явно сообщить о таком выполнении раннеру, добавив в тесты t.Parallel().

Два теста, выполняемых параллельно:

func TestParseDiskSpace(t *testing.T) {
    t.Parallel()
    ...
func TestParseMemoryUsage(t *testing.T) {
    t.Parallel()
    ...

Обнаружение состояний гонки и конструирование параллельного кода — обширная, интересная тема, которую мы обсудим в будущем.

Сканирование исходного кода на уязвимости

govulncheck

У нас есть широкий выбор инструментов, которые сканируют кодовую базу на наличие известных уязвимостей, перечисленных в базе данных CVE.

Инструмент по умолчанию, который обеспечивает разработку и релиз безопасного кода, — govulncheck. Мы можем локально установить его на машине разработчика и так же запускать сканирование, прежде чем закоммитить и запушить код в удаленный репозиторий Git.

При желании можно интегрировать сканирование а конвейер CI на GitHub или GitLab. Затем можно сканировать каждый мерж-реквест, чтобы убедиться, что мы не создаем в проекте уязвимости.

govulncheck разработан командой Go. Информацию для сканера предоставляет специальная база данных уязвимостей Go. Локально установим govulncheck и попробуем основные функции.

Чтобы установить последнюю версию, выполните команду:

go install golang.org/x/vuln/cmd/govulncheck@latest

И пришло время проверить, хорошо ли прошла установка:

govulncheck -version
Go: go1.23.2
Scanner: [email protected]
DB: https://vuln.go.dev
DB updated: 2024-10-17 15:37:30 +0000 UTC
...

Мы готовы запустить первое сканирование. Клонируем git-репозиторий habit. Перейдите в его корневую директорию и запустите инструмент.

➜  habit git:(main) ✗ govulncheck
No vulnerabilities found.

Выглядит многообещающе! В исходном коде уязвимостей не найдено. Мы закончили? Не совсем! Мы собрали бинарник habit, когда файл go.mod определил версию Go 1.18, а текущая версия — v1.23.2.

Просканируем бинарный файл habit, а не исходный код:

➜ habit git:(main) ✗ govulncheck -mode binary -show verbose habit

Мы запускаем govulncheck в бинарном режиме. Это означает, что можно сканировать любой бинарный файл Go, к которому у нас есть доступ! Нам не нужен исходный код! Затем запускаем сканирование в подробном режиме. Оно покажет полный отчет, разбитый на несколько разделов. Последний аргумент — имя бинарного файла, который мы хотим просканировать.

Этот отчет выглядит по-другому! Что только что произошло?

Scanning your binary for known vulnerabilities...

Fetching vulnerabilities from the database...

Checking the binary against the vulnerabilities...

=== Symbol Results ===

No vulnerabilities found.

=== Package Results ===

Vulnerability #1: GO-2023-2186
    Incorrect detection of reserved device names on Windows in path/filepath
  More info: https://pkg.go.dev/vuln/GO-2023-2186
  Standard library
    Found in: path/[email protected]
    Fixed in: path/[email protected]

=== Module Results ===

Vulnerability #1: GO-2024-3107
    Stack exhaustion in Parse in go/build/constraint
  More info: https://pkg.go.dev/vuln/GO-2024-3107
  Standard library
    Found in: [email protected]
    Fixed in: [email protected]
...

Vulnerability #18: GO-2023-1878
    Insufficient sanitisation of Host header in net/http
  More info: https://pkg.go.dev/vuln/GO-2023-1878
  Standard library
    Found in: [email protected]
    Fixed in: [email protected]

Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 18
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.

В первом разделе содержится самое важное сообщение: уязвимости не найдены. Остальные разделы содержат информацию о других уязвимостях, обнаруженных в стандартных библиотеках Go. Окей, затронуты ли мы? Наша программа небезопасна? Итоговый отчет о сканировании говорит, что волноваться не следует. Похоже, программа не вызывает этих уязвимостей!

Your code is affected by 0 vulnerabilities.
This scan also found 1 vulnerability in packages you import and 18
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.

Обновим файл go.mod и изменим версию Go на последнюю 1.23. Далее запустим go mod tidy, чтобы обновить все зависимости — и мы готовы снова собрать двоичный файл.

➜ habit git:(main) ✗ go build -o habit cmd/main.go

Повторим сканирование:

➜  habit git:(main) ✗ govulncheck -mode binary -show verbose habit
Scanning your binary for known vulnerabilities...

Fetching vulnerabilities from the database...

Checking the binary against the vulnerabilities...

No vulnerabilities found.

Это то, чего мы хотели! Мы обновили версию Go, извлекли зависимости и убедились, что программное обеспечение и зависимости не содержат CVE.

gosec

gosec — статический анализатор кода. Он помогает находить в коде небезопасные конструкции. Мы можем установить его локально на ноутбук или запустить в конвейере CI как GitHub Action. Как уже сказано ранее, golangci-lint в качестве плагина содержит gosecи по умолчанию запускает его при каждом сканировании кода.

Попробуем установить сканер локально:

go install github.com/securego/gosec/v2/cmd/gosec@latest

Если вы не видите ошибок, gosec готов к действию. Прежде чем запустить первое сканирование, посмотрите меню:

gosec -h

gosec - Golang security checker

gosec analyses Go source code to look for common programming mistakes that
can lead to security problems.
...

Чтобы настроить поведение сканера, можно использовать длинный список опций и правил. Подробности о конкретных опциях выходят за рамки статьи.

Чтобы попробовать gosec, клонируйте репозиторий GitHub с кодом Go, который хотите просканировать. Клонируем репозиторий brutus. Это экспериментальное приложение для OSINT с открытым исходным кодом, оно тестирует конфигурацию веб-сервера.

git clone [email protected]:CyberRoute/bruter.git

Измените текущую директорию на корневую директорию проекта и запустите сканирование.

gosec /. ...

Через пару секунд gosec представит отчет о сканировании. Что можно изучить сразу? Видно список потенциальных проблем, отсортированных по серьезности и достоверности. Мы знаем, какая часть кода требует внимания и к какому классу уязвимостей относится проблема. Идеально! Что дальше?

...

[/.../bruter/pkg/fuzzer/randomua.go:69] - G404 (CWE-338): Use of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand) (Confidence: MEDIUM, Severity: HIGH)
    68:
  > 69:  randomIndex := rand.Intn(len(userAgents))
    70:  return userAgents[randomIndex]

...

[/.../bruter/pkg/server/config.go:40] - G402 (CWE-295): TLS InsecureSkipVerify set true. (Confidence: HIGH, Severity: HIGH)
    39:  customTransport := &http.Transport{
  > 40:   TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    41:  }

...

На этом этапе расследования можно проверить сообщенные CWE и узнать подробности о перечисленных уязвимостях. Например, вторая в списке проблема приводит нас на веб-сайт CWE-295, где об уязвимости можно узнать больше.

Фаззинг

Последний метод проверки качества кода и обнаружения уязвимостей — фазз-тестирование. Фаззинг — это особый вид автоматизированного тестирования. Случайно сгенерированные данные проходят через тестовое покрытие кода. Это крайне полезно, чтобы обнаружить потенциальные недостатки безопасности, таких как переполнение буфера, SQL-инъекции, DoS- и XSS-атаки. Самый важный атрибут фаззинга — то, что многие входные комбинации генерируются автоматически! Разработчикам не нужно ломать голову, пытаясь перебрать сотни, если не тысячи комбинаций входных данных. Это облегчение!

Мы сосредоточимся на фаззинге подробнее в следующих туториалах.

Большинство методов и техник тестирования, о которых мы говорили сегодня, поддерживаются фондом OpenSSF. Проекты с открытым исходным кодом, желающие получить значок Best Practice, должны соответствовать критериям FLOSS в таких областях, как лицензирование, управление изменениями, отчеты об уязвимостях, качество, безопасность, а также в областях статического и динамического анализа защищенности кода.

Будьте в безопасности, избавляйтесь от CVE и наслаждайтесь программированием!

Как говорит Джон Арундел:

«Программирование — это веселье, и вы должны веселиться!»

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Jakub Jarosz: Writing secure Go code

Предыдущая статьяУтечка мозгов в 3 часа дня: почему разработчики не могут продуктивно думать после обеда?
Следующая статьяКак создать импульсный эффект в Jetpack Compose