company_banner

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

Автор оригинала: Christian Meléndez
  • Перевод
В последние годы 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?

RUVDS.com
RUVDS – хостинг VDS/VPS серверов

Комментарии 26

    +10

    В принципе если не считать примеры на Go, то статья "6 рекомендаций по разработке безопасных <подставьте свое>-приложений"

      +6
      Тогда получится статья из прошлого примерно 15-летней давности.
        0
        ну а шестой пункт — «напрягайте булки, ведь ГОшка не умеет в исключения: берегите ноги»)))
        –2
        Первое, что тут стоит учитывать, заключается в том, что в Go нет исключений.

        Кто нибудь может объяснить чем panic не exception? Только тем что их не вызывают по поводу и без?


        Каждый раз такие зявления глаз режут.

          –2

          Формально panic нельзя обработать

            +5
            А recover?
              –2

              Ждал этого вопроса, но это не обработка исключения в классическом C++/Java. Нет типов исключений, это скорее не recover а supres.

                +2

                Как нет типа? В панику же любой параметр можно передать и потом как угодно обработать — включая проверку типа.

            +2
            panic вызывает прекращение выполнение горутины в которой был выполнен. обработать можно только падение горутины.
            экспешен может быть обработан на любом удобном программисту уровне.

            в некоторых случаях использование паники может выступать сурагатом неправильного и примитивного использования эксепшенов.
              0

              Спасибо.

                +2

                Ну как бы не совсем так.


                https://blog.golang.org/defer-panic-and-recover


                Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.

                Т.е. если сделать recover() то потом текущая функция вернётся а рутина продолжится как ни в чём не бывало. Правильно же?

                  0
                  panic запускает процесс раскручивания стека, вызывая отложенные defer функции. recover позволяет восстановить управления, останавливая этот процесс.
                  Т.е. это некий механизм для экстренного завершения горутины, с возможностью отмены, а не управляющая структура.
                    +1

                    Ну для меня возможность отмены выглядит вполне как управление. Т.е. можно добиться того же что и с исключениями просто возможно не так удобно потом как это намерянно не поощеряется использовать в таком ключе.

                      0
                      Нельзя. Вот пример работы с исключениями:
                      function run() {
                          throw new Exception();
                      }
                      
                      try {
                          run();
                          echo 'Done';
                      } catch(Exception $e) {
                         echo 'Error';
                      }
                      


                      А вот что такое panic/recover:
                      func run() {
                      	defer func() {
                                      if r := recover(); r != nil {
                                              print(r) // Error
                                      } 
                              }()
                      	panic("Error")
                      }
                      
                      func main() {
                      	run()
                      }
                      

                      Грубо говоря на тех же try-catch
                      function run() {
                          try {
                              throw new Exception();
                          } catch(Exception $e) {
                              echo 'Error';
                          }
                      }
                      
                      run();
                      


                      сравните первое с последним — у нас управление выводом ошибки переместилось в run(), а вывод «Done» вообще пропал т.к. мы не знаем состояние.
                        +1
                        управление выводом ошибки переместилось в run(), а вывод «Done» вообще пропал т.к. мы не знаем состояние

                        Всё, что у Вас пропало и переместилось, произошло не потому, что нельзя сделать как в 1-м случае, а потому что Вы так захотели. Не стоит делать выводы на таком основании. Ну а 1-й вариант на Go можете посмотреть в песочнице
                        try(func() {
                        	run()
                        	fmt.Print("Done")
                        }, func(e interface{}) {
                        	fmt.Print("Error")
                        })
                          0
                          отлично! лямбда вместо встроенного в язык механизма!) в ГОшку нарочно не стали включать эксепшины, чтобы люди колхозили их сами — прям как в старом-добром СИ ;)
                            0
                            Пример был сделан так, чтобы даже PHP-шнику стало понятно, что исключения в Go есть, а не для того, чтобы показать, как надо писать. Паники специально были засунуты в дальний угол, но не всем дано такое понять.
                              0
                              а не для того, чтобы показать, как надо писать
                              но именно это и является полезной информацией ИМХО
                                0
                                У Вас, как минимум, получилось другое поведение — где if с проверкой типа `e`, и повторный вызов panic? Давайте ещё через глобальную переменную сделаем, а может вообще через setjmp/longjmp? Вы показываете как можно «реализовать», но не говорите о том, что это непригодно для жизни.
                                  0
                                  Вы показываете как можно «реализовать», но не говорите о том, что это непригодно для жизни.

                                  А как Вы понимаете эти слова?
                                  Пример был сделан так, чтобы даже PHP-шнику стало понятно, что исключения в Go есть, а не для того, чтобы показать, как надо писать. Паники специально были засунуты в дальний угол


                                  У Вас, как минимум, получилось другое поведение — где if с проверкой типа `e`, и повторный вызов panic?

                                  Там же, где и в Вашем примере с panic. Ведь задача была показать, что исключения есть, а не сделать один в один.
                                  Пример был сделан так, чтобы даже PHP-шнику стало понятно

                                  Похоже, у меня не получилось, но я хотя бы попытался. Если Вы не знаете, как сделать проверку без глобальной переменной, или считаете, что из-за многословности это не исключения, то тут уж ничего не поделать.
                                  Вам, ведь, сразу человек написал:
                                  можно добиться того же что и с исключениями просто возможно не так удобно
                                  Вы ответили, что нельзя, продемонстрировав это ложным примером, а когда увидели контрпример, начали классическое повышение ставок.
                +1
                bcrypt, PDKDF2, Argon2, scrypt — это не алгоритмы шифрования.
                  +7
                  Go стал стандартом де-факто в области создания инструментов командной строки.
                  Можете обосновать это?
                    +5
                    В воспаленном воображении автора. Да и в 2020 писать о таких вещах это вообще дичь.
                    +3

                    6 примеров налитой воды от капитана Очевидности с целью пропихнуть свои продукты.

                      –1
                      Руководство как написать «Хайповую статью» для нового языка.

                      Возьми хайповый язык
                      возьми принципы безопасной разработки из PERL
                      адаптируй код
                      PROFIT
                        +2
                        Все новое — это хорошо забытое старое :)

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое