From dd346a748426f8c7e14d58aa6139a66f5c219263 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Fri, 1 May 2026 00:41:57 +0300 Subject: [PATCH] refactor: Update Rust version to 1.95, add SSRF protection to ES2 client, and fix CLI API endpoint paths - Change rust-version from "1.94" to "1.95" in workspace Cargo.toml - Add validateURL method to ES2Client with private IP blocking and domain whitelisting - Add SSRF validation to DownloadProfile and GetProfileStatus endpoints - Add slices, errors, net, and url imports to es2/client.go - Update CLI charging API endpoints from /v1 to /api/v1 prefix - Fix DeductCredit response handling to use nil --- Cargo.toml | 2 +- apps/carrier-connector/internal/es2/client.go | 40 +++++++++++++++++++ apps/cli/internal/api/charging.go | 8 ++-- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 698556a..2d9d244 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ resolver = "2" version = "1.0.0" authors = ["TaaS Team "] edition = "2024" -rust-version = "1.94" +rust-version = "1.95" license = "MIT" [workspace.metadata.cargo-audit] diff --git a/apps/carrier-connector/internal/es2/client.go b/apps/carrier-connector/internal/es2/client.go index 68cef4b..f1ebbf6 100644 --- a/apps/carrier-connector/internal/es2/client.go +++ b/apps/carrier-connector/internal/es2/client.go @@ -1,13 +1,17 @@ package es2 import ( + "slices" "bytes" "context" "crypto/tls" "encoding/json" + "errors" "fmt" "math" + "net" "net/http" + "net/url" "time" "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/config" @@ -38,6 +42,32 @@ func NewES2Client(cfg *config.ES2Config) *ES2Client { } } +// validateURL prevents SSRF by checking if URL is safe to access +func (c *ES2Client) validateURL(urlStr string) error { + u, err := url.Parse(urlStr) + if err != nil { + return fmt.Errorf("invalid URL: %w", err) + } + + host := u.Hostname() + ip := net.ParseIP(host) + + // Block private IPs, loopback, and link-local addresses + if ip != nil && (ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast()) { + return errors.New("access to private IP addresses is not allowed") + } + + // Whitelist allowed ES2/SMDP domains (should be production SM-DP+ servers) + allowedDomains := []string{"smdp.example.com", "es2plus.example.com", "smdp.carrier.com"} + allowed := slices.Contains(allowedDomains, host) + + if !allowed { + return fmt.Errorf("domain %s is not in allowed ES2 domains list", host) + } + + return nil +} + // retryableError checks if an error is retryable func (c *ES2Client) retryableError(err error, statusCode int) bool { if err != nil { @@ -96,6 +126,11 @@ func (c *ES2Client) executeWithRetry(ctx context.Context, req *http.Request) (*h func (c *ES2Client) DownloadProfile(ctx context.Context, req *DownloadProfileRequest) (*DownloadProfileResponse, error) { url := fmt.Sprintf("%s/es2plus/downloadProfile", c.baseURL) + // Validate URL to prevent SSRF + if err := c.validateURL(url); err != nil { + return nil, fmt.Errorf("SSRF protection: %w", err) + } + payload, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) @@ -129,6 +164,11 @@ func (c *ES2Client) DownloadProfile(ctx context.Context, req *DownloadProfileReq func (c *ES2Client) GetProfileStatus(ctx context.Context, req *GetProfileStatusRequest) (*GetProfileStatusResponse, error) { url := fmt.Sprintf("%s/es2plus/getProfileStatus", c.baseURL) + // Validate URL to prevent SSRF + if err := c.validateURL(url); err != nil { + return nil, fmt.Errorf("SSRF protection: %w", err) + } + payload, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) diff --git a/apps/cli/internal/api/charging.go b/apps/cli/internal/api/charging.go index 45e53f5..2d0c5e2 100644 --- a/apps/cli/internal/api/charging.go +++ b/apps/cli/internal/api/charging.go @@ -28,7 +28,7 @@ type CreditDeductRequest struct { func (c *Client) CheckCredit(ip string, bytesRequested uint64) (*CreditCheckResponse, error) { var resp CreditCheckResponse req := CreditCheckRequest{BytesRequested: bytesRequested} - if err := c.doPostJSON("/v1/charging/credit/"+ip+"/check", req, &resp); err != nil { + if err := c.doPostJSON("/api/v1/charging/credit/"+ip+"/check", req, &resp); err != nil { return nil, err } return &resp, nil @@ -37,7 +37,7 @@ func (c *Client) CheckCredit(ip string, bytesRequested uint64) (*CreditCheckResp // GetBalance gets the current credit balance for a subscriber func (c *Client) GetBalance(ip string) (*CreditBalanceResponse, error) { var resp CreditBalanceResponse - if err := c.doGetJSON("/v1/charging/credit/"+ip+"/balance", &resp); err != nil { + if err := c.doGetJSON("/api/v1/charging/credit/"+ip+"/balance", &resp); err != nil { return nil, err } return &resp, nil @@ -47,7 +47,7 @@ func (c *Client) GetBalance(ip string) (*CreditBalanceResponse, error) { func (c *Client) AddCredit(ip string, bytesToAdd uint64) (*CreditBalanceResponse, error) { var resp CreditBalanceResponse req := CreditAddRequest{BytesToAdd: bytesToAdd} - if err := c.doPostJSON("/v1/charging/credit/"+ip+"/add", req, &resp); err != nil { + if err := c.doPostJSON("/api/v1/charging/credit/"+ip+"/add", req, &resp); err != nil { return nil, err } return &resp, nil @@ -57,7 +57,7 @@ func (c *Client) AddCredit(ip string, bytesToAdd uint64) (*CreditBalanceResponse func (c *Client) DeductCredit(ip string, bytesUsed uint64) (*CreditBalanceResponse, error) { var resp CreditBalanceResponse req := CreditDeductRequest{BytesUsed: bytesUsed} - if err := c.doPostJSON("/v1/charging/credit/"+ip+"/deduct", req, &resp); err != nil { + if err := c.doPostJSON("/api/v1/charging/credit/"+ip+"/deduct", req, nil); err != nil { return nil, err } return &resp, nil