mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-21 02:30:14 +00:00
fix(security): restrict service monitors to probe tasks
This commit is contained in:
@@ -26,8 +26,11 @@ import (
|
||||
// code to their own origin and bind the victim's identity. A request Host is
|
||||
// trusted only when it is an operator-declared dashboard host (the same
|
||||
// allowlist that guards NAT routing). Otherwise the redirect is pinned to the
|
||||
// operator-declared DashboardHost; when DashboardHost is empty the operator has
|
||||
// not pinned a dashboard origin, so the request Host is passed through.
|
||||
// operator-declared DashboardHost. Empty DashboardHost intentionally retains
|
||||
// dynamic/multi-domain deployments by passing through request Host; those
|
||||
// deployments must validate Host at their trusted proxy and register exact
|
||||
// redirect URIs at the OAuth provider. GHSA-rf68-8gjr-36q7 documents this
|
||||
// configuration boundary and must be updated if this compatibility changes.
|
||||
func getRedirectURL(c *gin.Context) string {
|
||||
scheme := "http://"
|
||||
referer := c.Request.Referer()
|
||||
|
||||
@@ -261,8 +261,8 @@ func TestShowServiceFiltersCycleTransferStatsLikeServerList(t *testing.T) {
|
||||
assert.NoError(t, singleton.DB.Create(&model.Server{Common: model.Common{ID: 3, UserID: 200}, Name: "hidden member server", UUID: "hidden-member-server", HideForGuest: true}).Error)
|
||||
singleton.ServerShared = singleton.NewServerClass()
|
||||
|
||||
assert.NoError(t, singleton.DB.Create(&model.Service{Common: model.Common{ID: 10, UserID: 1}, Name: "shown service"}).Error)
|
||||
assert.NoError(t, singleton.DB.Create(&model.Service{Common: model.Common{ID: 11, UserID: 1}, Name: "hidden service", HideForGuest: true}).Error)
|
||||
assert.NoError(t, singleton.DB.Create(&model.Service{Common: model.Common{ID: 10, UserID: 1}, Name: "shown service", Type: model.TaskTypeTCPPing}).Error)
|
||||
assert.NoError(t, singleton.DB.Create(&model.Service{Common: model.Common{ID: 11, UserID: 1}, Name: "hidden service", Type: model.TaskTypeTCPPing, HideForGuest: true}).Error)
|
||||
|
||||
originalServiceSentinel := singleton.ServiceSentinelShared
|
||||
serviceSentinel, err := singleton.NewServiceSentinel(make(chan *model.Service, 2))
|
||||
|
||||
@@ -484,6 +484,9 @@ func createService(c *gin.Context) (uint64, error) {
|
||||
if err := c.ShouldBindJSON(&mf); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := model.ValidateServiceMonitorType(uint64(mf.Type)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !isValidServiceCover(mf.Cover) {
|
||||
return 0, singleton.Localizer.ErrorT("permission denied")
|
||||
@@ -548,6 +551,9 @@ func updateService(c *gin.Context) (any, error) {
|
||||
if err := c.ShouldBindJSON(&mf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := model.ValidateServiceMonitorType(uint64(mf.Type)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isValidServiceCover(mf.Cover) {
|
||||
return nil, singleton.Localizer.ErrorT("permission denied")
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/service/singleton"
|
||||
)
|
||||
|
||||
func serviceTypeSecurityRouter() *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(func(c *gin.Context) {
|
||||
setAuthUser(c, 100, model.RoleMember)
|
||||
c.Next()
|
||||
})
|
||||
r.POST("/api/v1/service", commonHandler(createService))
|
||||
r.PATCH("/api/v1/service/:id", commonHandler(updateService))
|
||||
return r
|
||||
}
|
||||
|
||||
func serviceTypeSecurityBody(taskType uint8) []byte {
|
||||
body, _ := json.Marshal(model.ServiceForm{
|
||||
Name: "service-type-security",
|
||||
Target: "example.invalid:443",
|
||||
Type: taskType,
|
||||
Cover: model.ServiceCoverIgnoreAll,
|
||||
SkipServers: map[uint64]bool{1: true},
|
||||
Duration: 30,
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
func TestCreateServiceRejectsNonProbeTaskTypes(t *testing.T) {
|
||||
setupCoverPATFixture(t)
|
||||
r := serviceTypeSecurityRouter()
|
||||
|
||||
for _, taskType := range []uint8{0, model.TaskTypeCommand, model.TaskTypeApplyConfig, model.TaskTypeExec, 255} {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/service", bytes.NewReader(serviceTypeSecurityBody(taskType)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
success, errMsg := decodeCommonResponseError(t, w.Body.Bytes())
|
||||
require.False(t, success, "type %d must be rejected", taskType)
|
||||
require.Contains(t, errMsg, "invalid service monitor type")
|
||||
}
|
||||
|
||||
var count int64
|
||||
require.NoError(t, singleton.DB.Model(&model.Service{}).Count(&count).Error)
|
||||
require.Zero(t, count, "rejected task types must not reach persistence")
|
||||
}
|
||||
|
||||
func TestUpdateServiceRejectsNonProbeTaskTypes(t *testing.T) {
|
||||
setupCoverPATFixture(t)
|
||||
r := serviceTypeSecurityRouter()
|
||||
service := &model.Service{
|
||||
Common: model.Common{UserID: 100},
|
||||
Name: "valid-service",
|
||||
Target: "example.invalid:443",
|
||||
Type: model.TaskTypeTCPPing,
|
||||
Cover: model.ServiceCoverIgnoreAll,
|
||||
SkipServers: map[uint64]bool{1: true},
|
||||
Duration: 30,
|
||||
}
|
||||
require.NoError(t, singleton.DB.Create(service).Error)
|
||||
|
||||
for _, taskType := range []uint8{model.TaskTypeCommand, model.TaskTypeApplyConfig, model.TaskTypeExec, 255} {
|
||||
w := httptest.NewRecorder()
|
||||
path := fmt.Sprintf("/api/v1/service/%d", service.ID)
|
||||
req := httptest.NewRequest(http.MethodPatch, path, bytes.NewReader(serviceTypeSecurityBody(taskType)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
success, errMsg := decodeCommonResponseError(t, w.Body.Bytes())
|
||||
require.False(t, success, "type %d must be rejected", taskType)
|
||||
require.Contains(t, errMsg, "invalid service monitor type")
|
||||
|
||||
var persisted model.Service
|
||||
require.NoError(t, singleton.DB.First(&persisted, service.ID).Error)
|
||||
require.Equal(t, uint8(model.TaskTypeTCPPing), persisted.Type)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user