6 рекомендаций по разработке безопасных Go-приложений +17


В последние годы Golang распространяется всё шире и шире. Успешные проекты, вроде Docker, Kubernetes и Terraform, сделали огромные ставки на этот язык программирования. Go стал стандартом де-факто в области создания инструментов командной строки. А если говорить о безопасности, то оказывается, что в этой сфере у Go всё в полнейшем порядке. А именно, с 2002 года в реестре CVE имеется запись лишь об одной уязвимости Golang.

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



Автор статьи, перевод которой мы сегодня публикуем, сформулировал, на основе данных OWASP, 6 рекомендаций по разработке безопасных приложений на Go.

1. Проверяйте данные, введённые пользователем


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

Для проверки пользовательского ввода можно использовать стандартные пакеты Go. Например, пакет strconv помогает производить преобразование строковых данных в данные других типов. Go, кроме того, поддерживает, благодаря regexp, регулярные выражения. Их можно использовать для реализации сложных сценариев проверки данных. Несмотря на то, что в среде разработки на Go предпочтение обычно отдаётся стандартным библиотекам, существуют и сторонние пакеты, направленные на проверку данных. Например — validator. С помощью этого пакета упрощается проверка сложных структур данных или отдельных значений. Например, в следующем коде показана проверка структуры User на предмет правильности содержащегося в ней адреса электронной почты:

package main

import (
  "fmt"

  "gopkg.in/go-playground/validator.v9"
)

type User struct {
  Email string `json:"email" validate:"required,email"`
  Name  string `json:"name" validate:"required"`
}

func main() {
  v := validator.New()
  a := User{
    Email: "a",
  }

  err := v.Struct(a)

  for _, e := range err.(validator.ValidationErrors) {
    fmt.Println(e)
  }
}

2. Используйте HTML-шаблоны


XSS (cross-site scripting, межсайтовый скриптинг) — это серьёзная и широко распространённая уязвимость. XSS-уязвимость позволяет атакующему внедрять в приложение вредоносный код, способный влиять на данные, генерируемые приложением. Например, некто может отправить приложению, в виде части строки запроса в URL, JavaScript-код. Когда приложение будет обрабатывать такой запрос, этот JavaScript-код может быть выполнен. В результате оказывается, что разработчику приложения стоит ожидать подобного и подвергать очистке данные, поступающие от пользователя.

Go имеет пакет html/template, позволяющий генерировать HTML-код, защищённый от внедрения вредоносных фрагментов. В результате браузер, выводящий атакованное приложение, вместо выполнения кода наподобие <script>alert(‘You’ve Been Hacked!’);</script>, сообщающего пользователю о том, что его взломали, будет воспринимать вредоносный JavaScript-код как обычный текст. Вот как выглядит HTTP-сервер, использующий HTML-шаблоны:

package main

import (
  "html/template"
  "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
  param1 := r.URL.Query().Get("param1")
  tmpl := template.New("hello")
  tmpl, _ = tmpl.Parse(`{{define "T"}}{{.}}{{end}}`)
  tmpl.ExecuteTemplate(w, "T", param1)
}
func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

Существуют и сторонние библиотеки, которые можно использовать при разработке веб-приложений на Go. Скажем, это Gorilla web toolkit. В этот набор инструментов входят библиотеки, которые помогают разработчику, например, кодировать значения в аутентификационных куки. А вот — ещё один проект — nosurf. Это — HTTP-пакет, который помогает предотвращать CSRF-атаки.

3. Защищайте проект от SQL-инъекций


Если вы — не новичок в веб-разработке, то вы, возможно, знаете об атаках методом SQL-инъекций (методом внедрения в запросы произвольного SQL-кода). Соответствующая уязвимость всё ещё занимает первую строчку в рейтинге OWASP Top 10. Для защиты приложений от SQL-инъекций нужно учитывать некоторые особенности. Так, первое, что нужно обеспечить, заключается в том, чтобы пользователь, который подключается к базе данных, имел бы ограниченные полномочия. Рекомендуется, кроме того, очищать данные, введённые пользователем, о чём мы уже говорили, или экранировать специальные символы и применять функцию HTMLEscapeString из пакета html/template.

Но самое важное в защите от SQL-инъекций — это использование параметризованных запросов (подготовленных выражений). В Go выражения подготавливают не для соединения, а для базы данных. Вот пример использования параметризованных запросов:

customerName := r.URL.Query().Get("name")
db.Exec("UPDATE creditcards SET name=? WHERE customerId=?", customerName, 233, 90)

А что если движок базы данных не поддерживает использование заранее подготовленных выражений? А как быть, если это воздействует на производительность запросов? В подобных случаях можно использовать функцию db.Query(), но сначала надо не забыть очистить пользовательский ввод. Для предотвращения атак методом SQL-инъекции можно воспользоваться и сторонними библиотеками — наподобие sqlmap.

Надо отметить, что, несмотря на все усилия по защите приложений от SQL-атак, иногда злоумышленникам всё же удаются эти атаки. Скажем — через внешние зависимости приложений. Для того чтобы повысить уровень защищённости проектов, можно использовать соответствующие средства для проверки безопасности приложений. Например — инструменты платформы Sqreen.

4. Шифруйте важную информацию


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

В рамках проекта OWASP сформулированы некоторые рекомендации относительно предпочтительных алгоритмов шифрования. Например, это bcrypt, PDKDF2, Argon2, scrypt. Существует пакет Go, crypto, который содержит надёжные реализации различных алгоритмов шифрования. Вот пример использования алгоритма bcrypt:

package main

import (
  "database/sql"
  "context"
  "fmt"

  "golang.org/x/crypto/bcrypt"
)

func main() {
  ctx := context.Background()
  email := []byte("john.doe@somedomain.com")
  password := []byte("47;u5:B(95m72;Xq")

  hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
  if err != nil {
    panic(err)
  }

  stmt, err := db.PrepareContext(ctx, "INSERT INTO accounts SET hash=?, email=?")
  if err != nil {
    panic(err)
  }
  result, err := stmt.ExecContext(ctx, hashedPassword, email)
  if err != nil {
    panic(err)
  }
}

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

5. Предусмотрите принудительное использование HTTPS


В наши дни большинство браузеров требуют, чтобы сайты, которые открывают с их помощью, поддерживали бы HTTPS. Chrome, например, покажет в строке адреса соответствующее уведомление в том случае, если обмен данными с сайтом ведётся без использования HTTPS. В организации, поддерживающей некий проект, может применяться политика безопасности, направленная на организацию защищённого обмена данными между сервисами, из которых состоит этот проект. В результате для обеспечения безопасности соединений нужно обращать внимание не только на приложение, прослушивающее порт 443. В проекте должно быть предусмотрено наличие соответствующих сертификатов, нужно организовать принудительное использование HTTPS для того, чтобы не дать атакующему возможность перейти на обмен данными по HTTP.

Вот пример приложения, которое принудительно использует HTTPS:

package main

import (
  "crypto/tls"
  "log"
  "net/http"
)

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
    w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
    w.Write([]byte("This is an example server.\n"))
  })
  cfg := &tls.Config{
    MinVersion:               tls.VersionTLS12,
    CurvePreferences:         []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
    PreferServerCipherSuites: true,
    CipherSuites: []uint16{
      tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
      tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
      tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
      tls.TLS_RSA_WITH_AES_256_CBC_SHA,
    },
  }
  srv := &http.Server{
    Addr:         ":443",
    Handler:      mux,
    TLSConfig:    cfg,
    TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
  }
  log.Fatal(srv.ListenAndServeTLS("tls.crt", "tls.key"))
}

Обратите внимание на то, что приложение прослушивает порт 443. А вот — строка, ответственная за принудительное использование HTTPS:

w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")

Кроме того, иметь смысл может указание имени сервера в конфигурации TLS:

config := &tls.Config{ServerName: "yourSiteOrServiceName"}

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

6. Внимательно относитесь к обработке ошибок и к логированию


Этот пункт последний в нашем списке, но, определённо, он далеко не последний по важности. Здесь речь идёт об обработке ошибок и о логировании.

Для того чтобы своевременно справляться с проблемами, возникающими в продакшне, нужно оснастить приложение соответствующими инструментами. При этом стоит очень внимательно отнестись к сообщениям об ошибках, которые показывают пользователям. Не стоит сообщать пользователю подробности о том, что пошло не так. Дело в том, что злоумышленник может воспользоваться этой информацией для того, чтобы узнать о том, какие сервисы и технологии используются в проекте. Более того, стоит помнить о том, что хотя логи обычно воспринимаются в позитивном свете, эти логи где-то хранятся. А если лог приложения попадёт не в те руки, анализ сведений из него может помочь в проведении атаки на проект.

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

if err != nil {
    // обработать ошибки
}

Кроме того, в Go есть стандартная библиотека для работы с логами, которая называется log. Вот простейший пример её использования:

package main

import (
  "log"
)

func main() {
  log.Print("Logging in Go!")
}

Существуют и сторонние библиотеки для организации логирования. Например — это logrus, glog, loggo. Вот небольшой пример использования logrus:

package main

import (
  "os"

  log "github.com/sirupsen/logrus"
)

func main() {
  file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND, 0644)
  if err != nil {
    log.Fatal(err)
  }

  defer file.Close()

  log.SetOutput(file)
  log.SetFormatter(&log.JSONFormatter{})
  log.SetLevel(log.WarnLevel)

  log.WithFields(log.Fields{
    "animal": "walrus",
    "size":   10,
  }).Info("A group of walrus emerges from the ocean")
}

И, наконец, применяйте при работе с данными, которые попадают в логи, те рекомендации по безопасности, которые мы уже обсуждали. В частности, очищайте и шифруйте такие данные.

Итоги


Рекомендации, приведённые здесь, это некий минимум, которым должен обладать проект, написанный на Go. Но если проект, о котором идёт речь, представляет собой утилиту командной строки, то в нём не нужно реализовывать защиту трафика, передаваемого по сети.  Остальные советы применимы к практически любым типам приложений. Если вы хотите глубоко изучить вопрос разработки защищённых приложений на Go — взгляните на книгу OWASP, посвящённую этому вопросу. А вот — репозиторий, который содержит ссылки на различные инструменты, направленные на обеспечение безопасности Go-приложений.

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

Уважаемые читатели! Как вы защищаете свои приложения, написанные на Go?




К сожалению, не доступен сервер mySQL