From 5d0ccb724b3e21d4da3e0553d29c2492aa514381 Mon Sep 17 00:00:00 2001 From: Alex Guerrieri Date: Tue, 27 Jan 2026 09:52:21 +0100 Subject: [PATCH 1/4] Add test case that captures SQL injection --- page_test.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/page_test.go b/page_test.go index 5b7ac37..40901d4 100644 --- a/page_test.go +++ b/page_test.go @@ -17,7 +17,7 @@ func TestPagination(t *testing.T) { MaxSize = 5 Sort = "ID" ) - paginator := pgkit.NewPaginator[T]( + paginator := pgkit.NewPaginator( pgkit.WithColumnFunc[T](strings.ToLower), pgkit.WithDefaultSize[T](DefaultSize), pgkit.WithMaxSize[T](MaxSize), @@ -45,3 +45,20 @@ func TestPagination(t *testing.T) { require.Len(t, result, MaxSize) require.Equal(t, &pgkit.Page{Page: 1, Size: MaxSize, More: true}, page) } + +func TestInvalidSort(t *testing.T) { + paginator := pgkit.NewPaginator[T]() + page := pgkit.NewPage(0, 0) + page.Sort = []pgkit.Sort{ + {Column: "ID; DROP TABLE users;", Order: pgkit.Asc}, + {Column: "name", Order: pgkit.Desc}, + } + + _, query := paginator.PrepareQuery(sq.Select("*").From("t"), page) + + sql, args, err := query.ToSql() + require.NoError(t, err) + require.Equal(t, "SELECT * FROM t ORDER BY name DESC LIMIT 11 OFFSET 0", sql) + require.Empty(t, args) + +} From 3c6bc48f0c66210de0f48252c8ddc47e0e80f200 Mon Sep 17 00:00:00 2001 From: Alex Guerrieri Date: Tue, 27 Jan 2026 09:52:24 +0100 Subject: [PATCH 2/4] Prevent SQL injection when passing sort --- page.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/page.go b/page.go index b3e5d4c..295c526 100644 --- a/page.go +++ b/page.go @@ -37,16 +37,20 @@ func (s Sort) String() string { return fmt.Sprintf("%s %s", s.Column, s.Order) } -var _MatcherOrderBy = regexp.MustCompile(`-?([a-zA-Z0-9]+)`) +func (s Sort) IsValid() bool { + return s.Column != "" && _MatcherOrderBy.MatchString(s.Column) +} + +var _MatcherOrderBy = regexp.MustCompile(`^-?([a-zA-Z_][a-zA-Z0-9_]*)$`) func NewSort(s string) (Sort, bool) { - if s == "" || !_MatcherOrderBy.MatchString(s) { - return Sort{}, false - } sort := Sort{ Column: s, Order: Asc, } + if !sort.IsValid() { + return Sort{}, false + } if strings.HasPrefix(s, "-") { sort.Column = s[1:] sort.Order = Desc @@ -79,7 +83,13 @@ func NewPage(size, page uint32, sort ...Sort) *Page { func (p *Page) GetOrder(defaultSort ...string) []Sort { // if page has sort, use it if p != nil && len(p.Sort) != 0 { - return p.Sort + sort := make([]Sort, 0, len(p.Sort)) + for _, s := range p.Sort { + if s.IsValid() { + sort = append(sort, s) + } + } + return sort } // if page has column, use default sort if p == nil || p.Column == "" { From a17f20fff8f1f0724615c0d85d7327b2453d592c Mon Sep 17 00:00:00 2001 From: Alex Guerrieri Date: Tue, 27 Jan 2026 13:37:03 +0100 Subject: [PATCH 3/4] Sanitize sort column to prevent SQL injection vulnerabilities --- page.go | 23 +++++++++-------------- page_test.go | 3 +-- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/page.go b/page.go index 295c526..d9207e4 100644 --- a/page.go +++ b/page.go @@ -6,6 +6,7 @@ import ( "strings" sq "github.com/Masterminds/squirrel" + "github.com/jackc/pgx/v5" ) const ( @@ -37,20 +38,16 @@ func (s Sort) String() string { return fmt.Sprintf("%s %s", s.Column, s.Order) } -func (s Sort) IsValid() bool { - return s.Column != "" && _MatcherOrderBy.MatchString(s.Column) -} - -var _MatcherOrderBy = regexp.MustCompile(`^-?([a-zA-Z_][a-zA-Z0-9_]*)$`) +var _MatcherOrderBy = regexp.MustCompile(`-?([a-zA-Z0-9]+)`) func NewSort(s string) (Sort, bool) { + if s == "" || !_MatcherOrderBy.MatchString(s) { + return Sort{}, false + } sort := Sort{ Column: s, Order: Asc, } - if !sort.IsValid() { - return Sort{}, false - } if strings.HasPrefix(s, "-") { sort.Column = s[1:] sort.Order = Desc @@ -83,13 +80,11 @@ func NewPage(size, page uint32, sort ...Sort) *Page { func (p *Page) GetOrder(defaultSort ...string) []Sort { // if page has sort, use it if p != nil && len(p.Sort) != 0 { - sort := make([]Sort, 0, len(p.Sort)) - for _, s := range p.Sort { - if s.IsValid() { - sort = append(sort, s) - } + for i, s := range p.Sort { + s.Column = pgx.Identifier(strings.Split(s.Column, ".")).Sanitize() + p.Sort[i] = s } - return sort + return p.Sort } // if page has column, use default sort if p == nil || p.Column == "" { diff --git a/page_test.go b/page_test.go index 40901d4..4ef175b 100644 --- a/page_test.go +++ b/page_test.go @@ -58,7 +58,6 @@ func TestInvalidSort(t *testing.T) { sql, args, err := query.ToSql() require.NoError(t, err) - require.Equal(t, "SELECT * FROM t ORDER BY name DESC LIMIT 11 OFFSET 0", sql) + require.Equal(t, "SELECT * FROM t ORDER BY \"ID; DROP TABLE users;\" ASC, \"name\" DESC LIMIT 11 OFFSET 0", sql) require.Empty(t, args) - } From 56dcf4011e21527b7f9403b724dec1d5558a041b Mon Sep 17 00:00:00 2001 From: Alex Guerrieri Date: Tue, 27 Jan 2026 13:40:27 +0100 Subject: [PATCH 4/4] PrepareRaw to paginate raw SQL queries --- page.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/page.go b/page.go index d9207e4..412da48 100644 --- a/page.go +++ b/page.go @@ -199,6 +199,26 @@ func (p Paginator[T]) PrepareQuery(q sq.SelectBuilder, page *Page) ([]T, sq.Sele return make([]T, 0, limit+1), q } +func (p Paginator[T]) PrepareRaw(q string, args []any, page *Page) ([]T, string, []any) { + limit, offset := page.Limit(), page.Offset() + + q = q + " ORDER BY " + strings.Join(p.getOrder(page), ", ") + q = q + " LIMIT @limit OFFSET @offset" + + for i, arg := range args { + if existing, ok := arg.(pgx.NamedArgs); ok { + existing["limit"] = limit + 1 + existing["offset"] = offset + break + } + if i == len(args)-1 { + args = append(args, pgx.NamedArgs{"limit": limit + 1, "offset": offset}) + } + } + + return make([]T, 0, limit+1), q, args +} + // PrepareResult prepares the paginated result. If the number of rows is n+1: // - it removes the last element, returning n elements // - it sets more to true in the page object