Проверяем защищённость приложения на Go: с чего начать
Привет! Меня зовут Александра, я инженер по информационной безопасности в Delivery Club. Мы используем Go в качестве основного языка для разработки Web-API и представляем вашему вниманию краткое руководство по быстрой проверке сервиса на соответствие базовым требованиям безопасности. Представленную ниже информацию можно адаптировать под проекты, написанные и на других языках.
Код
Проверка пользовательского ввода
Первый и один из основных этапов анализа сервиса на соответствие требованиям безопасности — проверка пользовательского ввода. Мы ищем входные данные, принимаемые приложением, и определяем те из них, которым доверять нельзя, учитывая, что клиент является внешней сущностью. Такими данными могут быть:
заголовки HTTP-запросов;
тела HTTP-запросов;
URI-запросы, отвечающие за роутинг и/или маппинг ресурсов, при условии, что такой запрос каким-либо образом обрабатывается приложением;
параметры GET и POST;
содержимое форм.
Исключения
Исключениями из перечня данных пользовательского ввода будут параметры, использующие стойкую цифровую подпись с секретом, например, хранящиеся на сервере JWS (RFC 7515), подписанные и проверяемые алгоритмами на основе HMAC/RSA/ECDSA. Однако подобные параметры требуют особого внимания в связи с существованием атак на схемы шифрования и подписи, например:
signature removal — удаление цифровой подписи и модификация поля с указанием алгоритма;
crypto oracle — подпись произвольных данных.
Если в коде есть собственная реализация криптографии, малоизвестные алгоритмы или алгоритмы, криптографическая стойкость которых не доказана либо исследований которой не существует, то можно:
обратиться за консультацией к специалистам по криптографии;
провести собственное исследование на соответствие алгоритма требованиям информационной безопасности компании;
отказаться от использования подобных алгоритмов.
Как может выглядеть плохая обработка входных данных в псевдокоде:
http.HandleFunc("/bar", func(w HTTP.ResponseWriter, r *HTTP.Request) {
fmt.Fprintf(w, "Hello %q!", r.Url.Query.Get("name")) // XSS
ref := r.Headers.Get("Referrer") // недоверенный заголовок
if ref != "" {
resp, err := HTTP.Get(ref+"/?utm_source=backend") // SSRF
if err != nil {
fmt.Error(err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Error(err)
return
}
addToLogFile(string(body)) // запись недоверенных данных
}
})
log.Fatal(HTTP.ListenAndServe(":8080", nil))
Санитизация пользовательского ввода
Для защиты от вредоносной нагрузки чаще всего применяют санитизацию — удаление и/или экранирование неправильных или небезопасных символов из пользовательского ввода.
Задача первичной обработки входных данных лежит на HTTP-библиотеке. Во время проверки кода стоит обращать внимание на использование или отсутствие следующих методов санитизации:
белыx списков параметров (whitelisting), которые помогут отфильтровать невалидные или служебные параметры;
проверок границ значений (boundary checking) при конвертации строковых данных в числовые, и проверок ошибок конвертации;
проверок корректности конвертации строковых данных (Character escaping), например, проверок так называемого расширенного набора символов UTF-8, графически совпадающих с латинскими ASCII-символами.
проверок нуль-байтов;
проверок символов path-control: \ и \ \;
использование пакета «html/template» для безопасного отображения пользовательского ввода;
использование функции
Escape
* из пакета «template/html» для отображения спецсимволов.
Примечание: если валидация и/или санитизация входных данных невозможна, то HTTP-запрос должен быть полностью отклонен.
Пароли
Для реализации корректного механизма хранения и проверки паролей следует:
избегать использования устаревших алгоритмов, таких как SHA-1 и MD5;
использовать криптографически стойкий генератор псевдослучайных чисел.
Пример верной обработки пользовательских паролей:
package main
import (
"crypto/rand"
"crypto/sha256"
"database/sql"
"context"
"fmt"
)
const saltSize = 32
func main() {
ctx := context.Background()
email := []byte("john.doe@somedomain.com")
password := []byte("47;u5:B(95m72;Xq")
// создать случайное слово
salt := make([]byte, saltSize)
_, err := rand.Read(salt)
if err!=nil {
panic(err)
}
// SHA256(salt+password)
hash := sha256.New()
hash.Write(salt)
hash.Write(password)
h := hash.Sum(nil)
// fmt.Printf("email : %s\n", string(email))
// fmt.Printf("password: %s\n", string(password))
// fmt.Printf("salt : %x\n", salt)
// fmt.Printf("hash : %x\n", h)
// использовать при подключении к БД
stmt, err := db.PrepareContext(ctx, "INSERT INTO accounts SET hash=?, salt=?, email=?")
if err != nil {
panic(err)
}
result, err := stmt.ExecContext(ctx, h, salt, email)
if err != nil {
panic(err)
}
}
Примечание: идеальным вариантом является использование способов аутентификации, отличных от аутентификации по паролям.
Обработка ошибок
Необходимо правильно и своевременно обрабатывать ошибки по мере их появления, чтобы избежать ошибок бизнес-логики.
Пример обработки ошибок:
func initialize(i int) {
...
//Сбой
if i<2 {
fmt.Printf("Var %d - initialized\n", i)
} else {
//Завершаем нашу программу.
log.Fatal("Init failure - Terminating.")
}
}
func main() {
i:=1
for i<3 {
initialize(i)
i++
}
fmt.Println("Initialized all variables successfully")
}
Журналирование
Не допускайте включения чувствительных данных в журналы.
Передача данных между сервисами
Во избежание data-tampering и нелегитимного доступа к сервису требуется проверять:
Легитимность передачи информации между двумя сервисами (наличие доверительных отношений).
Целостность информации, передаваемой между сервисами.
Легитимность передачи информации между двумя сервисами достигается:
наличием информации об отправителе в белом списке получателя;
реализацией проверки доверия отношений между сервисами.
Целостность информации можно обеспечить с помощью алгоритмов цифровой подписи и имитовставки (DSA и HMAC). Для этой задачи может быть использован mTLS или любой другой алгоритм (IKE, SSH) двусторонней аутентификации.
Swagger
При проектировании микросервисов на основе Swagger рекомендуется проверять следующие критерии:
Соответствие схемы авторизации требованиям информационной безопасности — блок описания
securityDefinitions
(Swagger v2),components/securitySchemes
(Swagger v3) и провайдеров авторизации (Basic, OAuth2, Bearer).Наличие в блоке
security
информации о точках и методах описываемого API, требующих авторизацию, например:
security:
- basicAuth: ['/admin']
- apiKey: ['/v1/']
/ping:
get:
summary: Checks if the server is running
security: [] # No security
Примечание: если используемые методы авторизации для конечных точек и API вызывают сомнения, то необходимо уточнить корректность используемых схем.
TLS
Если при работе сервиса требуется использовать TLS, например, для связи с другими сервисами, то необходимо учитывать следующие критерии:
TLS Certificate Verification — в коде не должна использоваться конфигурация, игнорирующая проверку сертификатов. Пример неправильной конфигурации:
config := &tls.Config{InsecureSkipVerify: true}
config := &tls.Config{MinVersion:0, MaxVersion:1}
TLS ServerName — в случае конфигурации TLS HostName необходимо убедиться, что tls.ServerName совпадает с именем, указанным в сертификате:
config := &tls.Config{ServerName: "test-foo.com"}
Unverified TLS Library — рекомендуется использовать стандартную библиотеку для работы с TLS «crypto/tls». Если вы выбрали альтернативную библиотеку, убедитесь в её безопасности.
import "crypto/tls"
Источники
Мы считаем это руководство хорошей отправной точкой для обсуждения и дальнейшего построения аудита безопасности сервисов, в том числе написанных на Go.