Хабрахабр

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

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

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

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

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

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

Например, пакет strconv помогает производить преобразование строковых данных в данные других типов. Для проверки пользовательского ввода можно использовать стандартные пакеты Go. Их можно использовать для реализации сложных сценариев проверки данных. Go, кроме того, поддерживает, благодаря regexp, регулярные выражения. Например — validator. Несмотря на то, что в среде разработки на Go предпочтение обычно отдаётся стандартным библиотекам, существуют и сторонние пакеты, направленные на проверку данных. Например, в следующем коде показана проверка структуры 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() err := v.Struct(a) for _, e := range err.(validator.ValidationErrors) { fmt.Println(e) }
}

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

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

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

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

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, человеку не прочитать, это ещё не значит, что информация, скрытая в ней, надёжно защищена. Поэтому важную информацию нужно шифровать, защищая её от злоумышленников, которые могут получить доступ к зашифрованным данным. Обычно шифруют такую информацию, как пароли к базам данных, пользовательские пароли, персональные данные пользователей.

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

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?

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»