Ваша Go‑Fiber API может стать мишенью SSRF‑атак, если не валидировать пользовательские URL и не ограничить доступ к внутренним ресурсам. В этом руководстве мы разберём, как в Go‑Fiber реализовать защиту от SSRF, используя whitelist‑ы, таймауты и простую валидацию входных данных. Мы покажем готовый рабочий код, типичные ошибки и чек‑лист, чтобы ваш сайт был безопасен и надёжен.
Разбор задачи
SSRF (Server Side Request Forgery) – атака, когда злоумышленник заставляет ваш сервер выполнить запрос к произвольному адресу. В Go‑Fiber это часто случается, если ваше приложение принимает URL от клиента и делает HTTP‑запрос к нему. Если не ограничить диапазон допустимых адресов и не установить таймауты, сервер может стать прокси для внешних атак. Задача – гарантировать, что каждый внешний запрос идёт только к разрешённым адресам, с ограничением времени и проверкой схемы.
Пошаговое решение
- Определить whitelist доменов, к которым разрешён доступ. Это может быть список доменов, IP‑диапазонов или регулярных выражений.
- Создать middleware, который будет валидировать URL из запроса, проверять схему (http/https), домен и таймаут.
- Использовать context с таймаутом при выполнении HTTP‑запроса, чтобы не зависать на внешних сервисах.
- Логировать подозрительные запросы, чтобы можно было отследить попытки обхода.
Рабочий код
package main
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/gofiber/fiber/v2"
)
// whitelist содержит домены, к которым разрешён доступ
var whitelist = []string{"example.com", "api.service.com"}
// isAllowed проверяет, принадлежит ли URL домену из whitelist
func isAllowed(u *url.URL) bool {
host := strings.Split(u.Host, ":")[0] // убираем порт
for _, d := range whitelist {
if strings.EqualFold(host, d) {
return true
}
}
return false
}
// requestWithTimeout делает GET‑запрос с таймаутом 5 сек
func requestWithTimeout(ctx context.Context, target string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, "GET", target, nil)
if err != nil {
return nil, err
}
client := &http.Client{Timeout: 5 * time.Second}
return client.Do(req)
}
func main() {
app := fiber.New()
app.Get("/proxy", func(c *fiber.Ctx) error {
// 1. Получаем URL из query‑параметра
rawURL := c.Query("url")
if rawURL == "" {
return c.Status(fiber.StatusBadRequest).SendString("url param missing")
}
// 2. Парсим URL
u, err := url.Parse(rawURL)
if err != nil {
return c.Status(fiber.StatusBadRequest).SendString("invalid url")
}
// 3. Проверяем схему
if u.Scheme != "http" && u.Scheme != "https" {
return c.Status(fiber.StatusBadRequest).SendString("only http/https allowed")
}
// 4. Проверяем whitelist
if !isAllowed(u) {
return c.Status(fiber.StatusForbidden).SendString("domain not allowed")
}
// 5. Выполняем запрос с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := requestWithTimeout(ctx, u.String())
if err != nil {
// Логируем ошибку, но не раскрываем детали клиенту
fmt.Printf("SSRF attempt: %sn", err)
return c.Status(fiber.StatusBadGateway).SendString("external service error")
}
defer resp.Body.Close()
// 6. Передаём тело ответа клиенту
return c.Status(resp.StatusCode).Stream(func(w fiber.ResponseWriter) {
io.Copy(w, resp.Body)
})
})
app.Listen(":3000")
}
