Skip to content

Commit 3e99c03

Browse files
authored
fix: improve pagination parser (#12422)
1 parent 61db293 commit 3e99c03

File tree

5 files changed

+101
-7
lines changed

5 files changed

+101
-7
lines changed

coderd/httpapi/queryparams.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,30 @@ func (p *QueryParamParser) Int(vals url.Values, def int, queryParam string) int
7979
return v
8080
}
8181

82+
// PositiveInt32 function checks if the given value is 32-bit and positive.
83+
//
84+
// We can't use `uint32` as the value must be within the range <0,2147483647>
85+
// as database expects it. Otherwise, the database query fails with `pq: OFFSET must not be negative`.
86+
func (p *QueryParamParser) PositiveInt32(vals url.Values, def int32, queryParam string) int32 {
87+
v, err := parseQueryParam(p, vals, func(v string) (int32, error) {
88+
intValue, err := strconv.ParseInt(v, 10, 32)
89+
if err != nil {
90+
return 0, err
91+
}
92+
if intValue < 0 {
93+
return 0, xerrors.Errorf("value is negative")
94+
}
95+
return int32(intValue), nil
96+
}, def, queryParam)
97+
if err != nil {
98+
p.Errors = append(p.Errors, codersdk.ValidationError{
99+
Field: queryParam,
100+
Detail: fmt.Sprintf("Query param %q must be a valid 32-bit positive integer (%s)", queryParam, err.Error()),
101+
})
102+
}
103+
return v
104+
}
105+
82106
func (p *QueryParamParser) Boolean(vals url.Values, def bool, queryParam string) bool {
83107
v, err := parseQueryParam(p, vals, strconv.ParseBool, def, queryParam)
84108
if err != nil {

coderd/httpapi/queryparams_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,50 @@ func TestParseQueryParams(t *testing.T) {
236236
testQueryParams(t, expParams, parser, parser.Int)
237237
})
238238

239+
t.Run("PositiveInt32", func(t *testing.T) {
240+
t.Parallel()
241+
expParams := []queryParamTestCase[int32]{
242+
{
243+
QueryParam: "valid_integer",
244+
Value: "100",
245+
Expected: 100,
246+
},
247+
{
248+
QueryParam: "empty",
249+
Value: "",
250+
Expected: 0,
251+
},
252+
{
253+
QueryParam: "no_value",
254+
NoSet: true,
255+
Default: 5,
256+
Expected: 5,
257+
},
258+
{
259+
QueryParam: "negative",
260+
Value: "-1",
261+
Expected: 0,
262+
Default: 5,
263+
ExpectedErrorContains: "must be a valid 32-bit positive integer",
264+
},
265+
{
266+
QueryParam: "invalid_integer",
267+
Value: "bogus",
268+
Expected: 0,
269+
ExpectedErrorContains: "must be a valid 32-bit positive integer",
270+
},
271+
{
272+
QueryParam: "max_int_plus_one",
273+
Value: "2147483648",
274+
Expected: 0,
275+
ExpectedErrorContains: "must be a valid 32-bit positive integer",
276+
},
277+
}
278+
279+
parser := httpapi.NewQueryParamParser()
280+
testQueryParams(t, expParams, parser, parser.PositiveInt32)
281+
})
282+
239283
t.Run("UInt", func(t *testing.T) {
240284
t.Parallel()
241285
expParams := []queryParamTestCase[uint64]{

coderd/pagination.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ func parsePagination(w http.ResponseWriter, r *http.Request) (p codersdk.Paginat
1717
parser := httpapi.NewQueryParamParser()
1818
params := codersdk.Pagination{
1919
AfterID: parser.UUID(queryParams, uuid.Nil, "after_id"),
20-
// Limit default to "-1" which returns all results
21-
Limit: parser.Int(queryParams, 0, "limit"),
22-
Offset: parser.Int(queryParams, 0, "offset"),
20+
Limit: int(parser.PositiveInt32(queryParams, 0, "limit")),
21+
Offset: int(parser.PositiveInt32(queryParams, 0, "offset")),
2322
}
2423
if len(parser.Errors) > 0 {
2524
httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{

coderd/pagination_internal_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,31 @@ func TestPagination(t *testing.T) {
4646
Limit: "bogus",
4747
ExpectedError: invalidValues,
4848
},
49+
{
50+
Name: "TooHighLimit",
51+
Limit: "2147483648",
52+
ExpectedError: invalidValues,
53+
},
54+
{
55+
Name: "NegativeLimit",
56+
Limit: "-1",
57+
ExpectedError: invalidValues,
58+
},
4959
{
5060
Name: "BadOffset",
5161
Offset: "bogus",
5262
ExpectedError: invalidValues,
5363
},
64+
{
65+
Name: "TooHighOffset",
66+
Offset: "2147483648",
67+
ExpectedError: invalidValues,
68+
},
69+
{
70+
Name: "NegativeOffset",
71+
Offset: "-1",
72+
ExpectedError: invalidValues,
73+
},
5474

5575
// Valid values
5676
{

coderd/workspaces_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"database/sql"
77
"encoding/json"
88
"fmt"
9+
"math"
910
"net/http"
1011
"os"
1112
"strings"
@@ -1727,33 +1728,39 @@ func TestOffsetLimit(t *testing.T) {
17271728
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
17281729
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
17291730

1730-
// empty finds all workspaces
1731+
// Case 1: empty finds all workspaces
17311732
ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
17321733
require.NoError(t, err)
17331734
require.Len(t, ws.Workspaces, 3)
17341735

1735-
// offset 1 finds 2 workspaces
1736+
// Case 2: offset 1 finds 2 workspaces
17361737
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
17371738
Offset: 1,
17381739
})
17391740
require.NoError(t, err)
17401741
require.Len(t, ws.Workspaces, 2)
17411742

1742-
// offset 1 limit 1 finds 1 workspace
1743+
// Case 3: offset 1 limit 1 finds 1 workspace
17431744
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
17441745
Offset: 1,
17451746
Limit: 1,
17461747
})
17471748
require.NoError(t, err)
17481749
require.Len(t, ws.Workspaces, 1)
17491750

1750-
// offset 3 finds no workspaces
1751+
// Case 4: offset 3 finds no workspaces
17511752
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
17521753
Offset: 3,
17531754
})
17541755
require.NoError(t, err)
17551756
require.Len(t, ws.Workspaces, 0)
17561757
require.Equal(t, ws.Count, 3) // can't find workspaces, but count is non-zero
1758+
1759+
// Case 5: offset out of range
1760+
ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
1761+
Offset: math.MaxInt32 + 1, // Potential risk: pq: OFFSET must not be negative
1762+
})
1763+
require.Error(t, err)
17571764
}
17581765

17591766
func TestWorkspaceUpdateAutostart(t *testing.T) {

0 commit comments

Comments
 (0)