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

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

1. Шифрование данных: их защита при хранении и при передаче

Шифрованием хранимых данных обеспечивается, что, даже завладев чужой базой данных, злоумышленник не получит конфиденциальную информацию. Данные легко шифруются на Go с помощью удобного пакета crypto/aes, вот пример AES-шифрования:

package main

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"io"
)

func encrypt(key []byte, plaintext string) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}

ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}

stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], []byte(plaintext))

return hex.EncodeToString(ciphertext), nil
}

Шифрование при передаче  —  это защита данных, передаваемых через интернет. Безопасность передаваемых данных обеспечивается протоколом защиты транспортного уровня, на Go это легко благодаря http.ListenAndServeTLS:

package main

import (
"log"
"net/http"
)

func main() {
err := http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

2. Предотвращение SQL-инъекции

SQL-инъекция  —  все равно что передача хакеру ключей от базы данных. Предотвращается она подготовленными операторами, на Go для этого имеется пакет database/sql:

package main

import (
"database/sql"
_ "github.com/lib/pq"
)

func getUser(db *sql.DB, username string) (string, error) {
var name string
stmt, err := db.Prepare("SELECT name FROM users WHERE username = $1")
if err != nil {
return "", err
}
defer stmt.Close()

err = stmt.QueryRow(username).Scan(&name)
return name, err
}

Подготовленные операторы  —  это гарантия того, что вредоносный SQL-код не проберется в ваши запросы.

3. Предотвращение межсайтового скриптинга

Атаки межсайтового скриптинга случаются, когда злоумышленники внедряют в веб-страницы вредоносный код, выполняемый затем другими пользователями. Пакетом html/template на Go опасные символы обходятся автоматически:

package main

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

var tmpl = template.Must(template.New("example").Parse(`
<html>
<body>
<h1>{{.}}</h1>
</body>
</html>
`))

func handler(w http.ResponseWriter, r *http.Request) {
userInput := r.FormValue("input")
tmpl.Execute(w, userInput)
}

func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}

Так предотвращается внедрение вредоносного JavaScript на сайт.

4. Защита от CSRF-атак

Межсайтовой подделкой запроса пользователи принуждаются к выполнению нежелательных для них действий. Чтобы защитить формы, пакетом gorilla/csrf, к запросам автоматически добавляются CSRF-токены:

package main

import (
"net/http"
"github.com/gorilla/csrf"
)

func main() {
csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"))

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<form method="POST" action="/submit"><input type="hidden" name="` + csrf.FieldName + `" value="` + csrf.Token(r) + `"><input type="submit" value="Submit"></form>`))
})

http.ListenAndServe(":8000", csrfMiddleware(http.DefaultServeMux))
}

CSRF-токены  —  все равно что секретные ключи, по которым убеждаешься: запрос отправлен пользователем, а не злоумышленником.

5. Контроль доступа и JWT-аутентификация

Реализацией ролевого контроля доступа и проверки подлинности веб-токенов JSON обеспечивается получение пользователями доступа только к тому, что им разрешено. На Go эти токены легко генерируются и проверяются библиотекойjwt-go:

package main

import (
"time"
"github.com/dgrijalva/jwt-go"
)

var jwtKey = []byte("my_secret_key")

func createToken(username string, role string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
Username: username,
Role: role,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtKey)
}

Так аутентифицируются пользователи и на основе их ролей контролируется доступ.

6. Настройка безопасных заголовков

Надежность приложения повышается дополнительным уровнем защиты безопасных HTTP-заголовков, добавляемых в Go промежуточным слоем:

package main

import (
"net/http"
)

func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
next.ServeHTTP(w, r)
})
}

func main() {
http.Handle("/", securityHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Secure headers applied!"))
})))

http.ListenAndServe(":8080", nil)
}

Этими заголовками предотвращаются кликджекинг и попытки при помощи анализа характеристик ресурса угадать правильный MIME-тип, а также обеспечиваются безопасные соединения.

7. Логирование и мониторинг

Важно отслеживать, что происходит в приложении: структурированным логированием с logrus и мониторингом при помощи инструментов вроде Prometheus. Вот простая настройка логирования:

package main

import (
log "github.com/sirupsen/logrus"
)

func main() {
log.SetFormatter(&log.JSONFormatter{})
log.SetLevel(log.InfoLevel)

log.WithFields(log.Fields{
"event": "user_login",
"user": "johndoe",
}).Info("User logged in")
}

С logrus события регистрируются в структурированном формате, который потом легко парсить.

8. Обеспечение безопасности зависимостей

Важно регулярно обновлять модули Go и искать в них уязвимости. Обновления проверяются так:

go list -m -u all

А уязвимости в приложении ищутся инструментами вроде OWASP ZAP или Burp Suite.

9. Принцип наименьших привилегий

При подключении к базе данных пользователю предоставляются минимальные привилегии, необходимые для этой операции. Так, если нужно только считывание данных, применяется «пользователь только для чтения»:

db, err := sql.Open("postgres", "user=readonly dbname=mydb sslmode=disable")
if err != nil {
log.Fatal(err)
}

10. Безопасность API: ограничение скорости

Ограничением скорости предотвращаются злонамеренные действия с API, пакет x/time/rate идеален для этого:

package main

import (
"golang.org/x/time/rate"
"net/http"
)

var limiter = rate.NewLimiter(1, 5)

func rateLimiter(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}

func main() {
http.Handle("/", rateLimiter(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})))
http.ListenAndServe(":8080", nil)
}

С этим простым ограничителем скорости API не перегружается запросами пользователей.

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Seno Wijayanto: Securing Your Go Backend: Encryption, Vulnerability Prevention, and More!

Предыдущая статьяПрактики фронтенд-разработки, которые помогут избежать неудач
Следующая статьяПредварительный просмотр Jetpack Compose-анимации по ключевым кадрам в Android Studio