summaryrefslogtreecommitdiff
path: root/services/worker/internal/fetcher
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-08 09:23:43 -0800
committerFuwn <[email protected]>2026-02-08 09:23:43 -0800
commit56cbd35136a5a7b366835bf6c662ed068f6b5dec (patch)
tree1a6dc83f997683341ed3476d8f38690bfe7b7114 /services/worker/internal/fetcher
parentsecurity: sanitize HTML in marketing demo (diff)
downloadasa.news-56cbd35136a5a7b366835bf6c662ed068f6b5dec.tar.xz
asa.news-56cbd35136a5a7b366835bf6c662ed068f6b5dec.zip
security: harden Go worker
- Fix SSRF TOCTOU: add custom dialer that resolves DNS and validates IPs at connection time, preventing DNS rebinding attacks - Handle IPv4-mapped IPv6 addresses (::ffff:127.0.0.1) in SSRF protection by normalizing to IPv4 before checking reserved ranges - Sanitize feed error messages before storing: strip credentials from URLs and truncate to 500 chars - Remove unused EncryptionKey from configuration - Add stack trace logging to worker panic recovery for debugging - Run go fmt
Diffstat (limited to 'services/worker/internal/fetcher')
-rw-r--r--services/worker/internal/fetcher/fetcher.go30
-rw-r--r--services/worker/internal/fetcher/ssrf_protection.go7
2 files changed, 35 insertions, 2 deletions
diff --git a/services/worker/internal/fetcher/fetcher.go b/services/worker/internal/fetcher/fetcher.go
index bbeb622..48dcb1e 100644
--- a/services/worker/internal/fetcher/fetcher.go
+++ b/services/worker/internal/fetcher/fetcher.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io"
+ "net"
"net/http"
"time"
)
@@ -21,9 +22,36 @@ type Fetcher struct {
}
func NewFetcher(fetchTimeout time.Duration) *Fetcher {
+ safeDialer := &net.Dialer{
+ Timeout: 10 * time.Second,
+ }
+
+ safeTransport := &http.Transport{
+ DialContext: func(dialContext context.Context, network, address string) (net.Conn, error) {
+ host, port, splitError := net.SplitHostPort(address)
+ if splitError != nil {
+ return nil, fmt.Errorf("invalid address %q: %w", address, splitError)
+ }
+
+ resolvedAddresses, lookupError := net.DefaultResolver.LookupIPAddr(dialContext, host)
+ if lookupError != nil {
+ return nil, fmt.Errorf("failed to resolve %q: %w", host, lookupError)
+ }
+
+ for _, resolved := range resolvedAddresses {
+ if isReservedAddress(resolved.IP) {
+ return nil, fmt.Errorf("blocked connection to reserved IP address")
+ }
+ }
+
+ return safeDialer.DialContext(dialContext, network, net.JoinHostPort(host, port))
+ },
+ }
+
return &Fetcher{
httpClient: &http.Client{
- Timeout: fetchTimeout,
+ Timeout: fetchTimeout,
+ Transport: safeTransport,
CheckRedirect: func(request *http.Request, previousRequests []*http.Request) error {
if len(previousRequests) >= 5 {
return fmt.Errorf("too many redirects (exceeded 5)")
diff --git a/services/worker/internal/fetcher/ssrf_protection.go b/services/worker/internal/fetcher/ssrf_protection.go
index e88c8d7..86c9077 100644
--- a/services/worker/internal/fetcher/ssrf_protection.go
+++ b/services/worker/internal/fetcher/ssrf_protection.go
@@ -22,8 +22,13 @@ var reservedNetworks = []net.IPNet{
}
func isReservedAddress(ipAddress net.IP) bool {
+ normalizedIP := ipAddress
+ if ipv4 := ipAddress.To4(); ipv4 != nil {
+ normalizedIP = ipv4
+ }
+
for _, network := range reservedNetworks {
- if network.Contains(ipAddress) {
+ if network.Contains(normalizedIP) {
return true
}
}