summaryrefslogtreecommitdiff
path: root/services
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
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')
-rw-r--r--services/worker/internal/configuration/configuration.go3
-rw-r--r--services/worker/internal/fetcher/fetcher.go30
-rw-r--r--services/worker/internal/fetcher/ssrf_protection.go7
-rw-r--r--services/worker/internal/model/feed.go26
-rw-r--r--services/worker/internal/pool/pool.go2
-rw-r--r--services/worker/internal/writer/writer.go23
6 files changed, 71 insertions, 20 deletions
diff --git a/services/worker/internal/configuration/configuration.go b/services/worker/internal/configuration/configuration.go
index 84a5995..4d08196 100644
--- a/services/worker/internal/configuration/configuration.go
+++ b/services/worker/internal/configuration/configuration.go
@@ -15,7 +15,6 @@ type Configuration struct {
QueuePollInterval time.Duration
BatchSize int
HealthPort int
- EncryptionKey string
LogLevel string
LogJSON bool
}
@@ -33,7 +32,6 @@ func Load() (Configuration, error) {
queuePollInterval := getEnvironmentDuration("QUEUE_POLL_INTERVAL", 5*time.Second)
batchSize := getEnvironmentInteger("BATCH_SIZE", 50)
healthPort := getEnvironmentInteger("HEALTH_PORT", 8080)
- encryptionKey := os.Getenv("ENCRYPTION_KEY")
logLevel := getEnvironmentString("LOG_LEVEL", "info")
logJSON := getEnvironmentBoolean("LOG_JSON", false)
@@ -45,7 +43,6 @@ func Load() (Configuration, error) {
QueuePollInterval: queuePollInterval,
BatchSize: batchSize,
HealthPort: healthPort,
- EncryptionKey: encryptionKey,
LogLevel: logLevel,
LogJSON: logJSON,
}, nil
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
}
}
diff --git a/services/worker/internal/model/feed.go b/services/worker/internal/model/feed.go
index a903af0..db7a913 100644
--- a/services/worker/internal/model/feed.go
+++ b/services/worker/internal/model/feed.go
@@ -5,19 +5,19 @@ import (
)
type Feed struct {
- Identifier string
- URL string
- SiteURL *string
- Title *string
- FeedType *string
- Visibility string
- EntityTag *string
- LastModified *string
- LastFetchedAt *time.Time
- LastFetchError *string
- ConsecutiveFailures int
- NextFetchAt time.Time
- FetchIntervalSeconds int
+ Identifier string
+ URL string
+ SiteURL *string
+ Title *string
+ FeedType *string
+ Visibility string
+ EntityTag *string
+ LastModified *string
+ LastFetchedAt *time.Time
+ LastFetchError *string
+ ConsecutiveFailures int
+ NextFetchAt time.Time
+ FetchIntervalSeconds int
CreatedAt time.Time
UpdatedAt time.Time
SubscriberUserIdentifier *string
diff --git a/services/worker/internal/pool/pool.go b/services/worker/internal/pool/pool.go
index 7df03e2..0576636 100644
--- a/services/worker/internal/pool/pool.go
+++ b/services/worker/internal/pool/pool.go
@@ -3,6 +3,7 @@ package pool
import (
"context"
"log/slog"
+ "runtime/debug"
"sync"
)
@@ -38,6 +39,7 @@ func (workerPool *WorkerPool) Submit(workContext context.Context, workFunction W
workerPool.logger.Error(
"worker panic recovered",
"panic_value", recoveredPanic,
+ "stack_trace", string(debug.Stack()),
)
}
}()
diff --git a/services/worker/internal/writer/writer.go b/services/worker/internal/writer/writer.go
index 748deb0..fb413e0 100644
--- a/services/worker/internal/writer/writer.go
+++ b/services/worker/internal/writer/writer.go
@@ -3,10 +3,12 @@ package writer
import (
"context"
"fmt"
- "github.com/Fuwn/asa-news/internal/model"
- "github.com/jackc/pgx/v5/pgxpool"
+ "net/url"
"strings"
"time"
+
+ "github.com/Fuwn/asa-news/internal/model"
+ "github.com/jackc/pgx/v5/pgxpool"
)
type Writer struct {
@@ -192,11 +194,28 @@ func (feedWriter *Writer) UpdateFeedType(
return nil
}
+func sanitizeErrorMessage(errorMessage string) string {
+ sanitized := errorMessage
+ for _, word := range strings.Fields(errorMessage) {
+ if parsed, parseError := url.Parse(word); parseError == nil && parsed.User != nil {
+ parsed.User = nil
+ sanitized = strings.ReplaceAll(sanitized, word, parsed.String())
+ }
+ }
+
+ if len(sanitized) > 500 {
+ sanitized = sanitized[:500]
+ }
+
+ return sanitized
+}
+
func (feedWriter *Writer) RecordFeedError(
updateContext context.Context,
feedIdentifier string,
errorMessage string,
) error {
+ errorMessage = sanitizeErrorMessage(errorMessage)
currentTime := time.Now().UTC()
updateQuery := `
UPDATE feeds