feat(backend): Add query guardrails to prevent potential issues (#2149)
## Summary Implements query guardrails in the backend to prevent execution of expensive or malformed queries that could impact customer environments. Part of https://github.com/grafana/oss-big-tent-squad/issues/127 ## Changes ### New guardrails added: 1. **Item ID validation** (`queryItemIdData`) - Validates that item IDs are non-empty - Validates that item IDs contain only numeric values 2. **Time range validation** (`QueryData`) - Validates that `From` timestamp is before `To` timestamp 3. **API method allowlist** (`ZabbixAPIHandler`) - Only allows Zabbix API methods defined in the frontend type `zabbixMethodName` - Blocks any write/delete/update operations not in the allowlist ### New files: - `pkg/datasource/guardrails.go` - Validation functions and error definitions - `pkg/datasource/guardrails_test.go` - Unit tests for all validation functions ### Modified files: - `pkg/datasource/datasource.go` - Added time range validation - `pkg/datasource/zabbix.go` - Added item ID validation - `pkg/datasource/resource_handler.go` - Added API method validation ## Reasoning - Allowed functions might be unnecessary as we've already prevent using those in [types.ts](https://github.com/grafana/grafana-zabbix/blob/main/src/datasource/zabbix/types.ts#L1-L23) but it's nice to be cautious. - itemid and time validation is just for sanity. - Time range validation will be necessary in the future to warn user agains running expensive queries.
This commit is contained in:
134
pkg/datasource/guardrails.go
Normal file
134
pkg/datasource/guardrails.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
)
|
||||
|
||||
// Guardrail errors
|
||||
var (
|
||||
ErrEmptyItemIDs = errors.New("itemids cannot be empty for item ID query mode")
|
||||
ErrInvalidItemID = errors.New("itemid must be a valid numeric value")
|
||||
ErrInvalidTimeRange = errors.New("invalid time range: 'from' must be before 'to'")
|
||||
ErrTimeRangeTooLarge = errors.New("time range exceeds maximum allowed duration")
|
||||
ErrTooManyItems = errors.New("query would return too many items")
|
||||
ErrAPIMethodNotAllowed = errors.New("API method is not allowed")
|
||||
)
|
||||
|
||||
// Default guardrail limits
|
||||
// These limits help prevent performance issues and excessive resource consumption.
|
||||
// Based on Zabbix best practices:
|
||||
// - Zabbix "Max period for time selector" defaults to 2 years (range: 1-10 years)
|
||||
// See: https://www.zabbix.com/documentation/current/en/manual/web_interface/frontend_sections/administration/general
|
||||
// - Zabbix search_limit parameter defaults to 1000 items
|
||||
// See: https://www.zabbix.com/documentation/current/en/manual/api/reference/settings/object
|
||||
//
|
||||
// Our limits are conservative to ensure good performance:
|
||||
const (
|
||||
DefaultMaxTimeRangeDays = 365 // 1 year
|
||||
DefaultMaxItems = 500
|
||||
)
|
||||
|
||||
// ValidateItemIDs validates that itemids string is not empty and contains valid numeric IDs
|
||||
func ValidateItemIDs(itemids string) error {
|
||||
trimmed := strings.TrimSpace(itemids)
|
||||
if trimmed == "" {
|
||||
return backend.DownstreamError(ErrEmptyItemIDs)
|
||||
}
|
||||
|
||||
// Split and validate each ID
|
||||
ids := strings.Split(trimmed, ",")
|
||||
numericPattern := regexp.MustCompile(`^\d+$`)
|
||||
hasValidID := false
|
||||
|
||||
for _, id := range ids {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue // Skip empty entries from trailing commas
|
||||
}
|
||||
if !numericPattern.MatchString(id) {
|
||||
return backend.DownstreamError(ErrInvalidItemID)
|
||||
}
|
||||
hasValidID = true
|
||||
}
|
||||
|
||||
if !hasValidID {
|
||||
return backend.DownstreamError(ErrEmptyItemIDs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateTimeRange validates that the time range is valid (from < to)
|
||||
func ValidateTimeRange(timeRange backend.TimeRange) error {
|
||||
if !timeRange.From.Before(timeRange.To) {
|
||||
return backend.DownstreamError(ErrInvalidTimeRange)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateTimeRangeDuration validates that the time range doesn't exceed the maximum allowed duration
|
||||
func ValidateTimeRangeDuration(timeRange backend.TimeRange, maxDays int) error {
|
||||
if maxDays <= 0 {
|
||||
maxDays = DefaultMaxTimeRangeDays
|
||||
}
|
||||
|
||||
duration := timeRange.To.Sub(timeRange.From)
|
||||
maxDuration := time.Duration(maxDays) * 24 * time.Hour
|
||||
|
||||
if duration > maxDuration {
|
||||
return backend.DownstreamError(ErrTimeRangeTooLarge)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateItemCount validates that the number of items doesn't exceed the maximum allowed
|
||||
func ValidateItemCount(count int, maxItems int) error {
|
||||
if maxItems <= 0 {
|
||||
maxItems = DefaultMaxItems
|
||||
}
|
||||
|
||||
if count > maxItems {
|
||||
return backend.DownstreamError(ErrTooManyItems)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllowedAPIMethods defines the Zabbix API methods that are allowed to be called.
|
||||
// This list should be kept in sync with src/datasource/zabbix/types.ts (zabbixMethodName type)
|
||||
var AllowedAPIMethods = map[string]bool{
|
||||
"alert.get": true,
|
||||
"apiinfo.version": true,
|
||||
"application.get": true,
|
||||
"event.acknowledge": true,
|
||||
"event.get": true,
|
||||
"history.get": true,
|
||||
"host.get": true,
|
||||
"hostgroup.get": true,
|
||||
"item.get": true,
|
||||
"problem.get": true,
|
||||
"proxy.get": true,
|
||||
"script.execute": true,
|
||||
"script.get": true,
|
||||
"service.get": true,
|
||||
"service.getsla": true,
|
||||
"sla.get": true,
|
||||
"sla.getsli": true,
|
||||
"trend.get": true,
|
||||
"trigger.get": true,
|
||||
"user.get": true,
|
||||
"usermacro.get": true,
|
||||
"valuemap.get": true,
|
||||
}
|
||||
|
||||
// ValidateAPIMethod checks if the API method is in the allowed list
|
||||
func ValidateAPIMethod(method string) error {
|
||||
if _, allowed := AllowedAPIMethods[method]; !allowed {
|
||||
return backend.DownstreamError(ErrAPIMethodNotAllowed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user