Allow printable alert IDs for acknowledgements (#550)

This commit is contained in:
rcourtman 2025-10-14 16:48:22 +00:00
parent 7e5fa9a147
commit 0a5a4c1a0d
2 changed files with 49 additions and 6 deletions

View file

@ -36,18 +36,24 @@ func (h *AlertHandlers) SetMonitor(m *monitoring.Monitor) {
// validateAlertID validates an alert ID for security
func validateAlertID(alertID string) bool {
// Check length to prevent DOS attacks with huge IDs
// Guard against empty strings or extremely large payloads that could impact memory usage.
if len(alertID) == 0 || len(alertID) > 500 {
return false
}
// Alert IDs should only contain alphanumeric, hyphens, underscores, colons, and slashes
// (e.g., "pve1:qemu/101-cpu", "node-offline-pve1")
for _, c := range alertID {
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == ':' || c == '/' || c == '.') {
// Reject attempts to traverse directories via crafted path segments.
if strings.Contains(alertID, "../") || strings.Contains(alertID, "/..") {
return false
}
// Permit any printable ASCII character (including spaces) so user supplied identifiers
// like instance names remain valid, while excluding control characters and DEL.
for _, r := range alertID {
if r < 32 || r == 127 {
return false
}
}
return true
}

View file

@ -0,0 +1,37 @@
package api
import "testing"
func TestValidateAlertID(t *testing.T) {
testCases := []struct {
name string
id string
valid bool
}{
{name: "basic", id: "guest-powered-off-pve-101", valid: true},
{name: "with spaces", id: "cluster one-node-101-cpu", valid: true},
{name: "with slash and colon", id: "pve1:qemu/101-cpu", valid: true},
{name: "empty", id: "", valid: false},
{name: "too long", id: string(make([]byte, 501)), valid: false},
{name: "control char", id: "bad\nvalue", valid: false},
{name: "path traversal", id: "../etc/passwd", valid: false},
{name: "path traversal middle", id: "pve/../secret", valid: false},
}
// Populate the oversized id string once to avoid zero bytes being mistaken for a valid character set.
for i := range testCases {
if testCases[i].name == "too long" {
value := make([]byte, 501)
for j := range value {
value[j] = 'a'
}
testCases[i].id = string(value)
}
}
for _, tc := range testCases {
if got := validateAlertID(tc.id); got != tc.valid {
t.Errorf("validateAlertID(%s) = %v, want %v", tc.name, got, tc.valid)
}
}
}