fix(controller): validate trigger task ownership

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
naiba
2026-05-17 10:24:19 +08:00
co-authored by naiba/CloudCode
parent e38a0ef469
commit 58f98db1a0
3 changed files with 97 additions and 0 deletions
+11
View File
@@ -2,6 +2,7 @@ package controller
import ( import (
"maps" "maps"
"slices"
"strconv" "strconv"
"time" "time"
@@ -192,5 +193,15 @@ func validateRule(c *gin.Context, r *model.AlertRule) error {
} else { } else {
return singleton.Localizer.ErrorT("need to configure at least a single rule") return singleton.Localizer.ErrorT("need to configure at least a single rule")
} }
// Trigger task IDs are user-controlled; validate them here so alerts cannot
// reference another user's cron and later execute it from the sentinel path.
if !singleton.CronShared.CheckPermission(c, slices.Values(r.FailTriggerTasks)) {
return singleton.Localizer.ErrorT("permission denied")
}
if !singleton.CronShared.CheckPermission(c, slices.Values(r.RecoverTriggerTasks)) {
return singleton.Localizer.ErrorT("permission denied")
}
return nil return nil
} }
+77
View File
@@ -1,12 +1,18 @@
package controller package controller
import ( import (
"net/http/httptest"
"testing" "testing"
"time" "time"
jwt "github.com/appleboy/gin-jwt/v2" jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/nezhahq/nezha/model"
"github.com/nezhahq/nezha/pkg/i18n"
"github.com/nezhahq/nezha/service/singleton"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
) )
func TestPayloadFunc(t *testing.T) { func TestPayloadFunc(t *testing.T) {
@@ -77,3 +83,74 @@ func TestIPBinding(t *testing.T) {
assert.Nil(t, claims["ip"]) assert.Nil(t, claims["ip"])
}) })
} }
func TestValidateRuleRejectsForeignTriggerTasks(t *testing.T) {
ctx := newMemberValidationContext(t)
alertRule := &model.AlertRule{
Common: model.Common{UserID: 200},
Name: "member alert",
Rules: []*model.Rule{{Type: "offline", Duration: 3}},
FailTriggerTasks: []uint64{42},
RecoverTriggerTasks: []uint64{42},
}
assert.Error(t, validateRule(ctx, alertRule))
}
func TestValidateServersRejectsForeignTriggerTasks(t *testing.T) {
ctx := newMemberValidationContext(t)
service := &model.Service{
Common: model.Common{UserID: 200},
Name: "member service",
EnableTriggerTask: true,
FailTriggerTasks: []uint64{42},
RecoverTriggerTasks: []uint64{42},
SkipServers: map[uint64]bool{},
}
assert.Error(t, validateServers(ctx, service))
}
func newMemberValidationContext(t *testing.T) *gin.Context {
t.Helper()
originalDB := singleton.DB
originalLoc := singleton.Loc
originalLocalizer := singleton.Localizer
originalCronShared := singleton.CronShared
originalServerShared := singleton.ServerShared
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
assert.NoError(t, db.AutoMigrate(&model.Cron{}, &model.Server{}))
assert.NoError(t, db.Create(&model.Cron{
Common: model.Common{ID: 42, UserID: 1},
Name: "foreign trigger task",
Command: "admin-maintenance",
TaskType: model.CronTypeTriggerTask,
Cover: model.CronCoverAlertTrigger,
}).Error)
singleton.DB = db
singleton.Loc = time.Local
singleton.Localizer = i18n.NewLocalizer("en_US", "nezha", "translations", i18n.Translations)
singleton.CronShared = singleton.NewCronClass()
singleton.ServerShared = singleton.NewServerClass()
t.Cleanup(func() {
singleton.DB = originalDB
singleton.Loc = originalLoc
singleton.Localizer = originalLocalizer
singleton.CronShared = originalCronShared
singleton.ServerShared = originalServerShared
})
gin.SetMode(gin.TestMode)
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
ctx.Set(model.CtxKeyAuthorizedUser, &model.User{
Common: model.Common{ID: 200},
Role: model.RoleMember,
})
return ctx
}
+9
View File
@@ -545,5 +545,14 @@ func validateServers(c *gin.Context, ss *model.Service) error {
return singleton.Localizer.ErrorT("permission denied") return singleton.Localizer.ErrorT("permission denied")
} }
// Trigger task IDs are user-controlled; validate them here so services cannot
// reference another user's cron and later execute it from the sentinel path.
if !singleton.CronShared.CheckPermission(c, slices.Values(ss.FailTriggerTasks)) {
return singleton.Localizer.ErrorT("permission denied")
}
if !singleton.CronShared.CheckPermission(c, slices.Values(ss.RecoverTriggerTasks)) {
return singleton.Localizer.ErrorT("permission denied")
}
return nil return nil
} }