fix(telegram): enforce transport policy and SSRF guard for media downloads

* fix(telegram): thread transport policy into media downloads with SSRF guard

* fix(telegram): trust configured APIServer during media downloads

* fix(telegram): use proper download timeout and clone DefaultTransport

- Clone http.DefaultTransport when proxy is configured to preserve
  connection pool, TLS handshake timeout, and keep-alive defaults
- Use dedicated 5-minute context timeout for media downloads instead
  of the shared 30s client timeout, preventing large file timeouts
  (local Bot API supports up to 200 MB)

---------

Co-authored-by: viettranx <viettranx@gmail.com>
This commit is contained in:
badgerbeesandviettranx authored and GitHub committed 2026-03-17 18:21:45 +07:00
1 parent 31ff5eefca
commit d46061405f
2 files changed
+37 -6

No files matched your search

+10 -5
View File
@@ -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,
+27 -1
View File
@@ -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)
}