refactor(api): prevent integer overflow in query parameters - #1180
refactor(api): prevent integer overflow in query parameters#1180sinchubhat wants to merge 2 commits into
Conversation
67f9a1a to
21ed5c2
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1180 +/- ##
==========================================
+ Coverage 50.09% 50.24% +0.14%
==========================================
Files 146 147 +1
Lines 13552 13614 +62
==========================================
+ Hits 6789 6840 +51
- Misses 6171 6181 +10
- Partials 592 593 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
0f52511 to
1a9abe7
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens the v1 HTTP API’s OData-style query parameters ($top, $skip, $count) by adding explicit parsing/validation to prevent integer overflow and invalid values from bubbling into 500 responses, aligning invalid input handling with consistent 400 Bad Request behavior.
Changes:
- Introduces
OData.BindAndValidatewith safe parsing and bounds checks for$top,$skip, and$count. - Switches multiple v1 list endpoints from
ShouldBindQueryto the new validation path. - Adds focused unit tests covering overflow, negatives, type errors, and bounds.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/controller/httpapi/v1/odata.go | Adds safe OData query binding/validation and supporting error types/constants. |
| internal/controller/httpapi/v1/odata_test.go | Adds unit tests for OData parsing/validation edge cases. |
| internal/controller/httpapi/v1/devices.go | Uses the new validation path for device listing and related handlers. |
| internal/controller/httpapi/v1/ciraconfigs.go | Uses the new validation path for CIRA config listing. |
| internal/controller/httpapi/v1/domains.go | Uses the new validation path for domain listing. |
| internal/controller/httpapi/v1/profiles.go | Uses the new validation path for profile listing. |
| internal/controller/httpapi/v1/wificonfigs.go | Uses the new validation path for wireless config listing. |
| internal/controller/httpapi/v1/ieee8021xconfigs.go | Uses the new validation path for IEEE 802.1x config listing. |
| internal/controller/httpapi/v1/auditlog.go | Uses the new validation path for event log listing. |
8ffcf8c to
faaf51c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
internal/controller/httpapi/v1/odata_test.go:19
- gin.SetMode modifies global Gin state. Because this test calls t.Parallel() before gin.SetMode, the SetMode call runs concurrently with other parallel tests and can introduce data races / cross-test interference under -race.
func TestOData_BindAndValidate(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
internal/controller/httpapi/v1/error.go:46
- When OData validation errors are wrapped in dto.NotValidError (via ErrValidation*.Wrap), this first validation case matches via errors.Is/As and returns err.Error() directly, bypassing notValidErrorHandle. That can surface less user-friendly/internal context and makes responses inconsistent across handlers.
switch {
case errors.As(err, &odataValidationErr) || errors.Is(err, ErrInvalidInteger) ||
errors.Is(err, ErrExceedsMaxRange) || errors.Is(err, ErrNegativeValue) || errors.Is(err, ErrInvalidBoolean):
msg := err.Error()
c.AbortWithStatusJSON(http.StatusBadRequest, response{Error: msg, Message: msg})
faaf51c to
cc07ffd
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
internal/controller/httpapi/v1/auditlog.go:95
- This route wraps OData query validation errors with ErrValidationProfile, which labels the error source as "ProfileAPI". That’s misleading for audit log/event log requests and makes debugging/log triage harder. Consider either using an audit-log-specific validation wrapper or just passing the BindAndValidate error directly to ErrorResponse (as the devices handlers do).
if err := odata.BindAndValidate(c); err != nil {
validationErr := ErrValidationProfile.Wrap("get", "BindAndValidate", err)
ErrorResponse(c, validationErr)
internal/controller/httpapi/v1/error.go:46
- Because dto.NotValidError unwraps to the underlying OData validation error, the first switch case will match (errors.Is/As) and respond with err.Error(). For NotValidError this uses consoleerrors.InternalError.Error(), which includes internal file/function/call details in the HTTP 400 response instead of the intended friendly message. Handle dto.NotValidError (and other wrapper validation types) before checking the underlying OData sentinels so wrapped errors don’t leak internal context.
switch {
case errors.As(err, &odataValidationErr) || errors.Is(err, ErrInvalidInteger) ||
errors.Is(err, ErrExceedsMaxRange) || errors.Is(err, ErrNegativeValue) || errors.Is(err, ErrInvalidBoolean):
msg := err.Error()
c.AbortWithStatusJSON(http.StatusBadRequest, response{Error: msg, Message: msg})
cc07ffd to
0940fe4
Compare
* Add validation to prevent integer overflow in query parameters (top, skip, count). * Returns 400 instead of 500 on invalid input.
0940fe4 to
5c9b7b8
Compare
|
|
||
| const ( | ||
| // MaxPageSize is the maximum allowed value for $top parameter. | ||
| MaxPageSize = 10000 |
There was a problem hiding this comment.
what is the source of this number, does it not look very high ?
There was a problem hiding this comment.
Suggest to keep it at 500
| // MaxPageSize is the maximum allowed value for $top parameter. | ||
| MaxPageSize = 10000 | ||
| // MaxSkipValue is the maximum allowed value for $skip parameter. | ||
| MaxSkipValue = 1000000 |
There was a problem hiding this comment.
what is the source of this number, does it not look very high ?
|
|
||
| func ErrorResponse(c *gin.Context, err error) { | ||
| // handleDomainErrors handles domain-specific errors. | ||
| func handleDomainErrors(c *gin.Context, err error) bool { |
There was a problem hiding this comment.
How is this change specific to this PR ?
| } | ||
|
|
||
| // handleTypedErrors handles remaining typed errors. | ||
| func handleTypedErrors(c *gin.Context, err error) { |
There was a problem hiding this comment.
How is this change specific to this PR ?
Test
Get token
TOKEN=$(curl -sk https://localhost:8181/api/v1/authorize -H "Content-Type: application/json" -d '{"username":"<username>","password":"<password>"}' | jq -r '.token')Test 1: Integer overflow on $top (should return 400, NOT 500)
Output:
Test 2: Integer overflow on $skip (should return 400, NOT 500)
Output:
Test 3: Negative value (should return 400)
Output:
Test 4: Exceeds max allowed (should return 400)
Output:
Test 5: Valid request (should return 200 with data)
Output:
HTTP/2 200 content-type: application/json; charset=utf-8 content-length: 1733 date: Fri, 07 Aug 2026 04:57:37 GMT {"totalCount":2,"data":[{"connectionStatus":false,"mpsInstance":"","hostname":"<host-ip-addr>","guid":"<guid>","mpsusername":"","tags":null,"tenantId":"","friendlyName":"MININT-67OALSL","dnsSuffix":"","deviceInfo":{"fwVersion":"20.0.5","fwBuild":"1628","fwSku":"16392","discovered":false,"currentMode":"not activated","features":"AMT Pro Corporate","ipAddress":"<ip-addr>","lastSynced":"2026-08-03T07:43:34.3493246Z","lmsInstalled":true,"lmsVersion":"2542.0.5.0","amtEnabledInBIOS":true,"meInterfaceVersion":"2542.0.52.0","dhcpEnabled":true,"osName":"windows","osVersion":"10.0.26100.8875 Build 26100.8875","osDistro":"Microsoft Windows 11 Enterprise 24H2","cpuModel":"Intel(R) Core(TM) Ultra 5 238V","osIpAddress":"<ip-addr>","ethernetAdapterCount":1,"monitorConnected":true},"username":"<username>","password":"","mpspassword":"","mebxpassword":"","useTLS":true,"allowSelfSigned":true,"certHash":""},{"connectionStatus":false,"mpsInstance":"","hostname":"<host-ip-addr>","guid":"<guid>","mpsusername":"","tags":null,"tenantId":"","friendlyName":"","dnsSuffix":"","deviceInfo":{"fwVersion":"","fwBuild":"","fwSku":"","discovered":true,"firstDiscovered":"2026-07-27T05:37:35.973182717Z","currentMode":"","features":"","ipAddress":"0.0.0.0","lastSynced":"2026-07-27T05:37:35.973182717Z","lmsInstalled":false,"meInterfaceVersion":"6.8.0-111-generic","osName":"linux","osVersion":"6.8.0-111-generic","osDistro":"Ubuntu 22.04.5 LTS","cpuModel":"Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz","osIpAddress":"<ip-addr>","ethernetAdapterCount":1,"monitorConnected":true},"username":"","password":"","mpspassword":"","mebxpassword":"","useTLS":false,"allowSelfSigned":false,"certHash":""}]}