Проверяем защищённость приложения на Go: с чего начать

ca9cc5b67bff29e1789dd3759fdff5c6.png

Привет! Меня зовут Александра, я инженер по информационной безопасности в 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.

© Habrahabr.ru