diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 829a577..6971f63 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -31,7 +31,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '1.26.4' + go-version: '1.26.5' # Initialize CodeQL. If this fails with ECONNRESET locally, consider using --toolcache in act. - name: Initialize CodeQL diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index 61696ad..e2d421e 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '1.26.4' + go-version: '1.26.5' - name: Build run: go build -v ./cmd/server @@ -42,4 +42,4 @@ jobs: go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... env: - GOTOOLCHAIN: go1.26.4 + GOTOOLCHAIN: go1.26.5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fc209f..4aa010b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '1.26.4' + go-version: '1.26.5' - name: Run GoReleaser uses: goreleaser/goreleaser-action@v7 diff --git a/.jules/sentinel.md b/.jules/sentinel.md index beda211..37ddc69 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -61,3 +61,8 @@ **Vulnerability:** Not a direct vulnerability, but overly long handlers increase cognitive load and the likelihood of security-critical logic (like permission checks or input validation) being overlooked or bypassed during future maintenance. **Learning:** Modularizing complex handlers into distinct "parse", "authorize", and "dispatch" phases clarifies the security boundaries and ensures that each step (authentication, authorization, validation) must succeed before any state-changing action is taken. **Prevention:** Regularly refactor handlers that exceed a reasonable length (e.g., 50 lines) into smaller, single-responsibility methods. + +## 2024-07-27 - SSRF Protection on External Source URLs +**Vulnerability:** The `AddExternalSource` handler in `internal/api/excluded_handlers.go` allowed unconstrained external URL submission without pre-flight checks, enabling potential Server-Side Request Forgery (SSRF) and Local Network sweeps. +**Learning:** This codebase maintains its own purpose-built `blocklist/internal/security` package for validating SSRF. Using standard URL parsing is not enough when we need to intercept DNS rebinding and internal IP resolutions. +**Prevention:** Always enforce URL schema limits and utilize the internal `security.IsSafeURL` function when validating user-provided external destinations before storing or fetching them. diff --git a/go.mod b/go.mod index 1b80d7c..200f6c3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module blocklist -go 1.26.4 +go 1.26.5 require ( github.com/alicebob/miniredis/v2 v2.38.0 @@ -127,7 +127,7 @@ require ( golang.org/x/net v0.56.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/text v0.39.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5bdc1de..68889a0 100644 --- a/go.sum +++ b/go.sum @@ -314,8 +314,8 @@ golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/internal/api/excluded_handlers.go b/internal/api/excluded_handlers.go index 56de3e7..85ebf9f 100644 --- a/internal/api/excluded_handlers.go +++ b/internal/api/excluded_handlers.go @@ -10,6 +10,7 @@ import ( "time" "blocklist/internal/models" + "blocklist/internal/security" "github.com/gin-gonic/gin" zlog "github.com/rs/zerolog/log" @@ -290,6 +291,22 @@ func (h *APIHandler) AddExternalSource(c *gin.Context) { return } + // Security: Input length validation to prevent unconstrained resource consumption + if len(req.Name) > 255 || len(req.SourceType) > 255 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Input fields exceed maximum length of 255 characters"}) + return + } + if len(req.URL) > 2048 { + c.JSON(http.StatusBadRequest, gin.H{"error": "URL exceeds maximum length of 2048 characters"}) + return + } + + if err := security.IsSafeURL(req.URL); err != nil { + zlog.Warn().Err(err).Str("url", req.URL).Msg("Attempted to add unsafe external source URL") + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or unsafe external source URL"}) + return + } + src := models.ExternalSource{ Name: req.Name, URL: req.URL, diff --git a/internal/repository/postgres.go b/internal/repository/postgres.go index 89989e2..01b5dd3 100644 --- a/internal/repository/postgres.go +++ b/internal/repository/postgres.go @@ -2,9 +2,9 @@ package repository import ( "blocklist/internal/models" - "github.com/bytedance/sonic" "errors" "fmt" + "github.com/bytedance/sonic" "strconv" "strings" "time" diff --git a/internal/repository/redis.go b/internal/repository/redis.go index c82889a..909d3b9 100644 --- a/internal/repository/redis.go +++ b/internal/repository/redis.go @@ -4,8 +4,8 @@ import ( "context" "crypto/rand" "encoding/hex" - "github.com/bytedance/sonic" "fmt" + "github.com/bytedance/sonic" "strconv" "strings" "time" diff --git a/internal/service/external_source_service.go b/internal/service/external_source_service.go index 108598a..9cf8717 100644 --- a/internal/service/external_source_service.go +++ b/internal/service/external_source_service.go @@ -4,8 +4,8 @@ import ( "blocklist/internal/models" "blocklist/internal/repository" "context" - "github.com/bytedance/sonic" "fmt" + "github.com/bytedance/sonic" "io" "net/http" "net/netip" diff --git a/internal/service/ip_service.go b/internal/service/ip_service.go index 6be961f..1bd7475 100644 --- a/internal/service/ip_service.go +++ b/internal/service/ip_service.go @@ -2,8 +2,8 @@ package service import ( "context" - "github.com/bytedance/sonic" "fmt" + "github.com/bytedance/sonic" "html" "net" "net/netip" @@ -1165,6 +1165,7 @@ type filterOptions struct { fromTime time.Time toTime time.Time } + func (s *IPService) prepareFilterOptions(query, country, addedBy, from, to string) *filterOptions { opts := &filterOptions{} if from != "" {