Безопасность Go приложений: топ-10 уязвимостей и методы защиты
Язык Go (Golang) завоевал популярность благодаря высокой производительности, простоте и встроенным механизмам безопасности. Однако, как и любой другой язык, он не защищает разработчика от всех возможных угроз. Ошибки проектирования, невнимательность при работе с внешними данными и неправильное использование стандартных библиотек могут привести к серьезным уязвимостям. В этой статье мы разберем топ-10 наиболее критических уязвимостей Go-приложений, приведем примеры кода и расскажем, как их избежать.
1. SQL-инъекции (SQL Injection)
Несмотря на наличие пакета database/sql, разработчики часто допускают конкатенацию строк при построении SQL-запросов. Это классическая уязвимость, которая позволяет злоумышленнику выполнить произвольный SQL-код.
Небезопасный код:
// ОПАСНО: конкатенация строк
func getUser(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
query := "SELECT * FROM users WHERE username = '" + username + "'"
rows, err := db.Query(query)
// ...
}
Безопасный код с параметризацией:
func getUser(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
// Используем плейсхолдеры
query := "SELECT * FROM users WHERE username = $1"
rows, err := db.Query(query, username)
// ...
}
Рекомендация: Всегда используйте параметризованные запросы. Никогда не конкатенируйте пользовательский ввод в SQL-строки.
2. Межсайтовый скриптинг (XSS)
Если ваше Go-приложение рендерит HTML на сервере (например, с помощью html/template), необходимо экранировать вывод. Встроенный пакет text/template этого не делает.
Небезопасный код:
// Использование text/template опасно
tmpl, _ := template.New("test").Parse("<h1>{{.Name}}</h1>")
tmpl.Execute(w, map[string]string{"Name": "<script>alert('XSS')</script>"})
Безопасный код:
// Всегда используйте html/template для HTML-шаблонов
import "html/template"
tmpl, _ := template.New("test").Parse("<h1>{{.Name}}</h1>")
tmpl.Execute(w, map[string]string{"Name": "<script>alert('XSS')</script>"})
// Вывод: <script>alert('XSS')</script>
Рекомендация: Для работы с HTML используйте только html/template. Всегда валидируйте и экранируйте пользовательский ввод, особенно если он выводится в атрибуты тегов или JavaScript.
3. Race Condition (Состояние гонки)
Go — язык с конкурентностью, и race condition — одна из самых частых проблем. Она может привести к повреждению данных, утечкам памяти и неожиданным сбоям.
Небезопасный код:
var counter int
func increment() {
for i := 0; i < 1000; i++ {
counter++ // Неатомарная операция
}
}
func main() {
go increment()
go increment()
}
Безопасный код с мьютексом:
var (
counter int
mu sync.Mutex
)
func increment() {
for i := 0; i < 1000; i++ {
mu.Lock()
counter++
mu.Unlock()
}
}
func main() {
go increment()
go increment()
}
Рекомендация: Используйте sync.Mutex, sync.RWMutex или атомарные операции (sync/atomic). Запускайте тесты с флагом -race для обнаружения гонок.
4. Path Traversal (Обход пути)
Уязвимость возникает, когда приложение использует пользовательский ввод для формирования пути к файлу без должной проверки.
Небезопасный код:
func serveFile(w http.ResponseWriter, r *http.Request) {
filename := r.URL.Query().Get("file")
http.ServeFile(w, r, "/var/www/"+filename) // Можно запросить ../../etc/passwd
}
Безопасный код:
import "path/filepath"
func serveFile(w http.ResponseWriter, r *http.Request) {
filename := r.URL.Query().Get("file")
// Очищаем путь и проверяем, что он внутри разрешенной директории
safePath := filepath.Clean("/var/www/" + filename)
if !strings.HasPrefix(safePath, "/var/www/") {
http.Error(w, "Invalid path", http.StatusForbidden)
return
}
http.ServeFile(w, r, safePath)
}
Рекомендация: Используйте filepath.Clean и проверяйте префикс пути. Не доверяйте пользовательскому вводу при работе с файловой системой.
5. Неправильная обработка ошибок и утечка информации
Go возвращает ошибки как значения, и разработчики часто возвращают их напрямую в HTTP-ответ, раскрывая внутреннюю структуру приложения.
Небезопасный код:
func connectDB() error {
// ...
return fmt.Errorf("failed to connect to database at 192.168.1.1: %w", err)
}
func handler(w http.ResponseWriter, r *http.Request) {
err := connectDB()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) // Утечка IP и деталей
}
}
Безопасный код:
func connectDB() error {
// ...
return fmt.Errorf("database connection error: %w", err)
}
func handler(w http.ResponseWriter, r *http.Request) {
err := connectDB()
if err != nil {
log.Printf("Internal error: %v", err) // Логируем детали
http.Error(w, "Internal Server Error", http.StatusInternalServerError) // Пользователю — общее сообщение
}
}
Рекомендация: Никогда не возвращайте пользователю сырые ошибки. Логируйте их для себя, а клиенту отдавайте общие сообщения.