From b2566813ac3c2a91d996d2f1fadbaee11eb72f44 Mon Sep 17 00:00:00 2001 From: alxeg Date: Sat, 14 Mar 2026 16:16:10 +0300 Subject: [PATCH] Check for valid callback source address --- config/payouts.properties | 1 + internal/api/payout/payout_handler.go | 34 ++++++++++++++++++- internal/service/yookassa/config/yookassa.go | 2 ++ internal/service/yookassa/module.go | 2 ++ internal/service/yookassa/yookassa_service.go | 5 +++ 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/config/payouts.properties b/config/payouts.properties index cec6667..71c5a46 100644 --- a/config/payouts.properties +++ b/config/payouts.properties @@ -40,6 +40,7 @@ Cache.TTL = 24h YooKassa.BaseUrl = https://api.yookassa.ru/v3 YooKassa.Timeout = 30s YooKassa.Test = false +YooKassa.AllowedCallbackSubnets = 185.71.76.0/27,185.71.77.0/27,77.75.153.0/25,77.75.156.11/32,77.75.156.35/32,77.75.154.128/25,2a02:5180::/32 # Base API key/secret YooKassa.ApiBaseKey = YooKassa.ApiBaseSecret = diff --git a/internal/api/payout/payout_handler.go b/internal/api/payout/payout_handler.go index b9704f3..bd16dcc 100644 --- a/internal/api/payout/payout_handler.go +++ b/internal/api/payout/payout_handler.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log/slog" + "net" "net/http" "regexp" @@ -15,6 +16,7 @@ import ( "payouts/internal/service/database" "payouts/internal/service/database/orm" "payouts/internal/service/yookassa" + yookassaConf "payouts/internal/service/yookassa/config" ) const ( @@ -30,6 +32,7 @@ type payoutHandler struct { dbService database.Service cacheService cache.Service yooKassa yookassa.Service + yookassaConf yookassaConf.YooKassa } // Params represents the module input params @@ -47,6 +50,7 @@ func NewPayoutHandler(p Params) (Handler, error) { dbService: p.DbService, cacheService: p.CacheService, yooKassa: p.YooKassa, + yookassaConf: p.YooKassa.GetConfig(), }, nil } @@ -70,6 +74,27 @@ func (p *payoutHandler) getSession(r *http.Request) (*orm.User, error) { } +func (p *payoutHandler) checkAllowedIpCallback(ipStr string) bool { + ipWithoutPort, _, _ := net.SplitHostPort(ipStr) + + ip := net.ParseIP(ipWithoutPort) + if ip == nil { + slog.Error(fmt.Sprintf("Invalid IP: %s", ipStr)) + return false + } + for _, subnetStr := range p.yookassaConf.AllowedCallbackSubnets { + _, ipNet, err := net.ParseCIDR(subnetStr) + if err != nil { + slog.Error(fmt.Sprintf("Invalid subnet CIDR: %v", err)) + continue + } + if ipNet.Contains(ip) { + return true + } + } + return false +} + // GetSbpBanks implements [Handler]. func (p *payoutHandler) GetSbpBanks(w http.ResponseWriter, r *http.Request) { panic("unimplemented") @@ -83,7 +108,7 @@ func (p *payoutHandler) PayoutCreate(w http.ResponseWriter, r *http.Request) { _, err := p.getSession(r) if err != nil { - errResponse("unautiorized", err, http.StatusUnauthorized) + errResponse("unauthorized", err, http.StatusUnauthorized) } panic("unimplemented") @@ -95,5 +120,12 @@ func (p *payoutHandler) PayoutCallback(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) decoder.Decode(&inData) + // todo: check also the X-real-ip and/or X-Forwarded-For + if !p.checkAllowedIpCallback(r.RemoteAddr) { + slog.Error(fmt.Sprintf("Callback came from unallowed ip: %s", r.RemoteAddr)) + http.Error(w, "unallowed", http.StatusForbidden) + return + } + slog.Info(fmt.Sprintf("Received callback from %s with object %v with headers %v", r.RemoteAddr, inData, r.Header)) } diff --git a/internal/service/yookassa/config/yookassa.go b/internal/service/yookassa/config/yookassa.go index 26ab533..fa40298 100644 --- a/internal/service/yookassa/config/yookassa.go +++ b/internal/service/yookassa/config/yookassa.go @@ -7,6 +7,8 @@ type YooKassa struct { Timeout time.Duration Test bool + AllowedCallbackSubnets []string + ApiBaseKey string ApiBaseSecret string ApiPaymentKey string diff --git a/internal/service/yookassa/module.go b/internal/service/yookassa/module.go index 6a0fdd1..639959b 100644 --- a/internal/service/yookassa/module.go +++ b/internal/service/yookassa/module.go @@ -8,6 +8,7 @@ import ( "payouts/internal/config" "payouts/internal/models" "payouts/internal/service/database/orm" + yookassaConf "payouts/internal/service/yookassa/config" ) var Module = fx.Options( @@ -27,6 +28,7 @@ func WithContext(ctx context.Context) Optional { type Service interface { CreatePayout(models.PayoutReq, *orm.User, ...Optional) + GetConfig() yookassaConf.YooKassa } type Param struct { diff --git a/internal/service/yookassa/yookassa_service.go b/internal/service/yookassa/yookassa_service.go index bd8e5f9..abff78f 100644 --- a/internal/service/yookassa/yookassa_service.go +++ b/internal/service/yookassa/yookassa_service.go @@ -65,3 +65,8 @@ func (y *yookassaService) CreatePayout(req models.PayoutReq, userSession *orm.Us y.payClient.PayoutsPost(params.ctx, &gen.PayoutRequest{}, gen.PayoutsPostParams{}) } + +// GetConfig implements [Service]. +func (y *yookassaService) GetConfig() config.YooKassa { + return y.conf +}