Ваша Go‑Fiber API может стать мишенью SSRF‑атак, если не валидировать пользовательские URL и не ограничить доступ к внутренним ресурсам. В этом руководстве мы разберём, как в Go‑Fiber реализовать защиту от SSRF, используя whitelist‑ы, таймауты и простую валидацию входных данных. Мы покажем готовый рабочий код, типичные ошибки и чек‑лист, чтобы ваш сайт был безопасен и надёжен.

Разбор задачи

SSRF (Server Side Request Forgery) – атака, когда злоумышленник заставляет ваш сервер выполнить запрос к произвольному адресу. В Go‑Fiber это часто случается, если ваше приложение принимает URL от клиента и делает HTTP‑запрос к нему. Если не ограничить диапазон допустимых адресов и не установить таймауты, сервер может стать прокси для внешних атак. Задача – гарантировать, что каждый внешний запрос идёт только к разрешённым адресам, с ограничением времени и проверкой схемы.

Пошаговое решение

  1. Определить whitelist доменов, к которым разрешён доступ. Это может быть список доменов, IP‑диапазонов или регулярных выражений.
  2. Создать middleware, который будет валидировать URL из запроса, проверять схему (http/https), домен и таймаут.
  3. Использовать context с таймаутом при выполнении HTTP‑запроса, чтобы не зависать на внешних сервисах.
  4. Логировать подозрительные запросы, чтобы можно было отследить попытки обхода.

Рабочий код

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")
}

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

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