diff --git a/internal/channels/telegram/channel.go b/internal/channels/telegram/channel.go index cd5a8712..f704aad0 100644 --- a/internal/channels/telegram/channel.go +++ b/internal/channels/telegram/channel.go @@ -23,6 +23,7 @@ type Channel struct { *channels.BaseChannel bot *telego.Bot config config.TelegramConfig + httpClient *http.Client pairingService store.PairingStore agentStore store.AgentStore // for group file writer management (nil if not configured) teamStore store.TeamStore // for /tasks, /task_detail commands (nil if not configured) @@ -61,17 +62,20 @@ func New(cfg config.TelegramConfig, msgBus *bus.MessageBus, pairingSvc store.Pai opts = append(opts, telego.WithAPIServer(cfg.APIServer)) } + httpClient := &http.Client{ + Timeout: 30 * time.Second, + } + if cfg.Proxy != "" { proxyURL, parseErr := url.Parse(cfg.Proxy) if parseErr != nil { return nil, fmt.Errorf("invalid proxy URL %q: %w", cfg.Proxy, parseErr) } - opts = append(opts, telego.WithHTTPClient(&http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyURL(proxyURL), - }, - })) + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = http.ProxyURL(proxyURL) + httpClient.Transport = transport } + opts = append(opts, telego.WithHTTPClient(httpClient)) bot, err := telego.NewBot(cfg.Token, opts...) if err != nil { @@ -95,6 +99,7 @@ func New(cfg config.TelegramConfig, msgBus *bus.MessageBus, pairingSvc store.Pai BaseChannel: base, bot: bot, config: cfg, + httpClient: httpClient, pairingService: pairingSvc, agentStore: agentStore, teamStore: teamStore, diff --git a/internal/channels/telegram/media.go b/internal/channels/telegram/media.go index 68a86029..91e434c9 100644 --- a/internal/channels/telegram/media.go +++ b/internal/channels/telegram/media.go @@ -14,6 +14,7 @@ import ( "github.com/mymmrac/telego" "github.com/nextlevelbuilder/goclaw/internal/channels/media" + "github.com/nextlevelbuilder/goclaw/internal/tools" ) const ( @@ -227,7 +228,32 @@ func (c *Channel) downloadMedia(ctx context.Context, fileID string, maxBytes int downloadURL = fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", c.config.Token, file.FilePath) } - resp, err := http.Get(downloadURL) + // SSRF Protection: check the resolved URL before connecting. + // We skip the check IF the host is our explicitly configured (trusted) API server. + isTrusted := c.config.APIServer != "" && strings.HasPrefix(downloadURL, c.config.APIServer) + if !isTrusted { + if err := tools.CheckSSRF(downloadURL); err != nil { + return "", fmt.Errorf("SSRF protection: %w", err) + } + } + + // Use a generous timeout for media downloads (large files via local Bot API + // can be up to 200 MB). The shared httpClient has a 30s timeout suited for + // API calls, so we override per-request with a dedicated context. + dlCtx, dlCancel := context.WithTimeout(ctx, 5*time.Minute) + defer dlCancel() + + req, err := http.NewRequestWithContext(dlCtx, "GET", downloadURL, nil) + if err != nil { + return "", fmt.Errorf("create download request: %w", err) + } + + // Clone the shared client without the 30s Timeout so the per-request + // context (5 min) governs the download duration instead. + dlClient := *c.httpClient + dlClient.Timeout = 0 + + resp, err := dlClient.Do(req) if err != nil { return "", fmt.Errorf("download file: %w", err) }