Post #15
31
ryzhov_dev На прошлых выходных принял участие в AvitoCTF и решил сделать райтап для задания МедоИИд 👍

Мой райтап неожиданно залетел в топ-10 и Авито прислали свой мерч 🎉
- 🎉 3
RY @ryzhov_dev
ryzhov_dev На прошлых выходных принял участие в AvitoCTF и решил сделать райтап для задания МедоИИд 👍

golang.org/x/time/rate — с него и стоит начинать.
func rateLimit(next http.Handler) http.Handler {
limiters := sync.Map{}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-API-Key")
l, _ := limiters.LoadOrStore(key, rate.NewLimiter(rate.Every(time.Second), 20))
if !l.(*rate.Limiter).Allow() {
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
CREATE INDEX idx_orders_report
ON orders (status, created_at, customer_id, total);
(status, created_at) не поможет запросу, где в WHERE только created_at. Перед тем как создавать — выпиши все запросы, которые он должен закрыть, и проверь, что префикс совпадает с их WHERE и ORDER BY.status, где 95% строк в одном значении, — это почти full scan с лишними блоками. Перед созданием — посчитай COUNT(DISTINCT ...) и оцени, что реально фильтруется первой колонкой.
proxy_pass. Один символ меняет всё:
location /api/ {
proxy_pass http://app/; # бэкенд видит /users
}
location /api/ {
proxy_pass http://app; # бэкенд видит /api/users
}
X-Forwarded-For и X-Forwarded-Proto — доверяй только тем, что проставил сам. То, что прислал клиент, — это пользовательский ввод, и на нём нельзя строить ни rate limiting, ни аудит.client_max_body_size против лимитов приложения — выигрывает меньший, а 413 прилетает от того, кто первый отказал.proxy_buffering on по умолчанию тихо ломает SSE и стриминг. Выключать надо точечно, на нужных location.proxy_read_timeout, proxy_send_timeout, keepalive_timeout, send_timeout — каждый рубит запрос на своей стадии. Половина дебага — понять, кто именно выстрелил.== или bytes.Equal. Это не constant-time, и теоретически подпись можно подобрать по утечкам времени. Надо hmac.Equal.
body, _ := io.ReadAll(r.Body)
ts := r.Header.Get("X-Signature-Timestamp")
unix, _ := strconv.ParseInt(ts, 10, 64)
if time.Since(time.Unix(unix, 0)).Abs() > 5*time.Minute {
return errors.New("stale")
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(ts + "."))
mac.Write(body)
got, _ := hex.DecodeString(r.Header.Get("X-Signature"))
if !hmac.Equal(got, mac.Sum(nil)) {
return errors.New("bad sig")
}
context.Context — это не "таймаут на запрос", это контракт: "вот сигнал остановиться, будь добр, услышь его". Если его игнорировать, работа продолжается, хотя результат уже никому не нужен. А ресурсы — коннекты к БД, горутины, память — висят.ctx.Done().http.Client без таймаута и без контекста в запросе.db.Query(...) вместо db.QueryContext(ctx, ...).
// было: горутина живёт дольше, чем нужно
func handler(w http.ResponseWriter, r *http.Request) {
go func() {
for {
doWork() // никто не останавливает
time.Sleep(time.Second)
}
}()
}
// стало: горутина слышит отмену и выходит
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
go func() {
for {
select {
case <-ctx.Done():
return
default:
doWork()
time.Sleep(time.Second)
}
}
}()
}
select с <-ctx.Done() горутина переживёт и хендлер, и смысл своего существования.composer audit, govulncheck — запускайте в CI. Уязвимая библиотека — это открытая дверь.
file, err := os.Open("config.yaml")
if err != nil {
return fmt.Errorf("failed to open config: %w", err)
}
if err != nil преследует во сне. Потом замечаешь: в проде перестают всплывать те самые "а откуда это прилетело?" ошибки. Стоит того.composer install на сервере. Собрал, скопировал, запустил. Кто танцевал с конфигами на пяти e-commerce площадках — тот поймёт, какое это облегчение.