fix(api): bound mysql page sizes

This commit is contained in:
lingniu
2026-07-16 08:49:10 +08:00
parent 99d9dcca1b
commit 251b53125d
2 changed files with 43 additions and 7 deletions

View File

@@ -13,8 +13,10 @@ type SQLQuery struct {
CountArgs []any CountArgs []any
} }
const maxSQLPageSize = 10_000
func buildVehicleListSQL(query url.Values) SQLQuery { func buildVehicleListSQL(query url.Values) SQLQuery {
limit := parsePositive(query.Get("limit"), 20) limit := parseSQLPageSize(query.Get("limit"), 20)
offset := parsePositive(query.Get("offset"), 0) offset := parsePositive(query.Get("offset"), 0)
canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols)) canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols))
args := []any{} args := []any{}
@@ -65,7 +67,7 @@ func buildVehicleListSQL(query url.Values) SQLQuery {
} }
func buildVehicleCoverageSQL(query url.Values) SQLQuery { func buildVehicleCoverageSQL(query url.Values) SQLQuery {
limit := parsePositive(query.Get("limit"), 20) limit := parseSQLPageSize(query.Get("limit"), 20)
offset := parsePositive(query.Get("offset"), 0) offset := parsePositive(query.Get("offset"), 0)
canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols)) canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols))
args := []any{} args := []any{}
@@ -288,7 +290,7 @@ func archiveMissingFieldPredicate(field string) string {
} }
func buildRealtimeLocationSQL(query url.Values) SQLQuery { func buildRealtimeLocationSQL(query url.Values) SQLQuery {
limit := parsePositive(query.Get("limit"), 20) limit := parseSQLPageSize(query.Get("limit"), 20)
offset := parsePositive(query.Get("offset"), 0) offset := parsePositive(query.Get("offset"), 0)
args := []any{} args := []any{}
where := []string{"1 = 1"} where := []string{"1 = 1"}
@@ -316,7 +318,7 @@ func buildRealtimeLocationSQL(query url.Values) SQLQuery {
} }
func buildVehicleRealtimeSQL(query url.Values) SQLQuery { func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
limit := parsePositive(query.Get("limit"), 20) limit := parseSQLPageSize(query.Get("limit"), 20)
offset := parsePositive(query.Get("offset"), 0) offset := parsePositive(query.Get("offset"), 0)
canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols)) canonicalSourceCount := strconv.Itoa(len(canonicalVehicleProtocols))
args := []any{} args := []any{}
@@ -415,7 +417,7 @@ func buildVehicleRealtimeSQL(query url.Values) SQLQuery {
} }
func buildDailyMileageSQL(query url.Values) SQLQuery { func buildDailyMileageSQL(query url.Values) SQLQuery {
limit := parsePositive(query.Get("limit"), 20) limit := parseSQLPageSize(query.Get("limit"), 20)
offset := parsePositive(query.Get("offset"), 0) offset := parsePositive(query.Get("offset"), 0)
args := []any{} args := []any{}
where := []string{"1 = 1"} where := []string{"1 = 1"}
@@ -671,11 +673,22 @@ func appendVINListFilter(where []string, args []any, column string, raw string)
} }
func buildLimitOffset(query url.Values) (int, int) { func buildLimitOffset(query url.Values) (int, int) {
return parsePositive(query.Get("limit"), 20), parsePositive(query.Get("offset"), 0) return parseSQLPageSize(query.Get("limit"), 20), parsePositive(query.Get("offset"), 0)
} }
func parseLimitOffset(rawLimit, rawOffset string) (int, int) { func parseLimitOffset(rawLimit, rawOffset string) (int, int) {
return parsePositive(rawLimit, 20), parsePositive(rawOffset, 0) return parseSQLPageSize(rawLimit, 20), parsePositive(rawOffset, 0)
}
func parseSQLPageSize(raw string, fallback int) int {
value, err := strconv.Atoi(raw)
if err != nil || value <= 0 {
return fallback
}
if value > maxSQLPageSize {
return maxSQLPageSize
}
return value
} }
func mustInt(value string) int { func mustInt(value string) int {

View File

@@ -319,6 +319,29 @@ func TestBuildDailyMileageSQLCanMatchStatisticsVehicleDayScope(t *testing.T) {
} }
} }
func TestMySQLListBuildersClampUntrustedPageSizes(t *testing.T) {
query := url.Values{"limit": {"1000000000"}, "offset": {"7"}}
builders := map[string]SQLQuery{
"vehicles": buildVehicleListSQL(query),
"vehicle coverage": buildVehicleCoverageSQL(query),
"realtime location": buildRealtimeLocationSQL(query),
"vehicle realtime": buildVehicleRealtimeSQL(query),
"daily mileage": buildDailyMileageSQL(query),
}
for name, built := range builders {
if len(built.Args) < 2 || built.Args[len(built.Args)-2] != maxSQLPageSize || built.Args[len(built.Args)-1] != 7 {
t.Fatalf("%s pagination was not clamped: %#v", name, built.Args)
}
}
limit, offset := buildLimitOffset(query)
if limit != maxSQLPageSize || offset != 7 {
t.Fatalf("response pagination metadata must match SQL: limit=%d offset=%d", limit, offset)
}
if limit, _ := parseLimitOffset("0", "0"); limit != 20 {
t.Fatalf("zero page size must use the safe default, got %d", limit)
}
}
func TestMileageQueriesRespectEnabledProtocolPriority(t *testing.T) { func TestMileageQueriesRespectEnabledProtocolPriority(t *testing.T) {
query := url.Values{ query := url.Values{
"protocols": {"JT808,GB32960"}, "protocols": {"JT808,GB32960"},