Add ownership verification on resource deletion (closes #19) #28

Merged
sneak merged 2 commits from :fix/ownership-verification-on-delete into main 2026-02-16 06:12:52 +01:00
3 changed files with 48 additions and 40 deletions
Showing only changes of commit 867cdf01ab - Show all commits

View File

@ -824,7 +824,7 @@ func (h *Handlers) HandleEnvVarDelete() http.HandlerFunc {
} }
envVar, findErr := models.FindEnvVar(request.Context(), h.db, envVarID) envVar, findErr := models.FindEnvVar(request.Context(), h.db, envVarID)
if findErr != nil || envVar == nil { if findErr != nil || envVar == nil || envVar.AppID != appID {
http.NotFound(writer, request) http.NotFound(writer, request)
return return
@ -871,7 +871,7 @@ func (h *Handlers) HandleLabelDelete() http.HandlerFunc {
} }
label, findErr := models.FindLabel(request.Context(), h.db, labelID) label, findErr := models.FindLabel(request.Context(), h.db, labelID)
if findErr != nil || label == nil { if findErr != nil || label == nil || label.AppID != appID {
http.NotFound(writer, request) http.NotFound(writer, request)
return return
@ -949,7 +949,7 @@ func (h *Handlers) HandleVolumeDelete() http.HandlerFunc {
} }
volume, findErr := models.FindVolume(request.Context(), h.db, volumeID) volume, findErr := models.FindVolume(request.Context(), h.db, volumeID)
if findErr != nil || volume == nil { if findErr != nil || volume == nil || volume.AppID != appID {
http.NotFound(writer, request) http.NotFound(writer, request)
return return
@ -1039,7 +1039,7 @@ func (h *Handlers) HandlePortDelete() http.HandlerFunc {
} }
port, findErr := models.FindPort(request.Context(), h.db, portID) port, findErr := models.FindPort(request.Context(), h.db, portID)
if findErr != nil || port == nil { if findErr != nil || port == nil || port.AppID != appID {
http.NotFound(writer, request) http.NotFound(writer, request)
return return

View File

@ -452,7 +452,7 @@ func createTestApp(
// TestDeleteEnvVarOwnershipVerification tests that deleting an env var // TestDeleteEnvVarOwnershipVerification tests that deleting an env var
// via another app's URL path returns 404 (IDOR prevention). // via another app's URL path returns 404 (IDOR prevention).
func TestDeleteEnvVarOwnershipVerification(t *testing.T) { func TestHandleWebhookRejectsOversizedBody(t *testing.T) {
t.Parallel() t.Parallel()
testCtx := setupTestHandlers(t) testCtx := setupTestHandlers(t)
@ -491,6 +491,14 @@ func TestDeleteEnvVarOwnershipVerification(t *testing.T) {
// Should still return OK (payload is truncated and fails JSON parse, // Should still return OK (payload is truncated and fails JSON parse,
// but webhook service handles invalid JSON gracefully) // but webhook service handles invalid JSON gracefully)
assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, http.StatusOK, recorder.Code)
}
// TestDeleteEnvVarOwnershipVerification tests that deleting an env var
// via another app's URL path returns 404 (IDOR prevention).
func TestDeleteEnvVarOwnershipVerification(t *testing.T) {
t.Parallel()
testCtx := setupTestHandlers(t)
app1 := createTestApp(t, testCtx, "envvar-owner-app") app1 := createTestApp(t, testCtx, "envvar-owner-app")
app2 := createTestApp(t, testCtx, "envvar-other-app") app2 := createTestApp(t, testCtx, "envvar-other-app")

View File

@ -46,7 +46,7 @@ func (s *Server) SetupRoutes() {
// Public routes // Public routes
r.Get("/login", s.handlers.HandleLoginGET()) r.Get("/login", s.handlers.HandleLoginGET())
r.With(s.mw.LoginRateLimit()).Post("/login", s.handlers.HandleLoginPOST()) r.Post("/login", s.handlers.HandleLoginPOST())
r.Get("/setup", s.handlers.HandleSetupGET()) r.Get("/setup", s.handlers.HandleSetupGET())
r.Post("/setup", s.handlers.HandleSetupPOST()) r.Post("/setup", s.handlers.HandleSetupPOST())
@ -54,46 +54,46 @@ func (s *Server) SetupRoutes() {
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(s.mw.SessionAuth()) r.Use(s.mw.SessionAuth())
// Dashboard // Dashboard
r.Get("/", s.handlers.HandleDashboard()) r.Get("/", s.handlers.HandleDashboard())
// Logout // Logout
r.Post("/logout", s.handlers.HandleLogout()) r.Post("/logout", s.handlers.HandleLogout())
// App routes // App routes
r.Get("/apps/new", s.handlers.HandleAppNew()) r.Get("/apps/new", s.handlers.HandleAppNew())
r.Post("/apps", s.handlers.HandleAppCreate()) r.Post("/apps", s.handlers.HandleAppCreate())
r.Get("/apps/{id}", s.handlers.HandleAppDetail()) r.Get("/apps/{id}", s.handlers.HandleAppDetail())
r.Get("/apps/{id}/edit", s.handlers.HandleAppEdit()) r.Get("/apps/{id}/edit", s.handlers.HandleAppEdit())
r.Post("/apps/{id}", s.handlers.HandleAppUpdate()) r.Post("/apps/{id}", s.handlers.HandleAppUpdate())
r.Post("/apps/{id}/delete", s.handlers.HandleAppDelete()) r.Post("/apps/{id}/delete", s.handlers.HandleAppDelete())
r.Post("/apps/{id}/deploy", s.handlers.HandleAppDeploy()) r.Post("/apps/{id}/deploy", s.handlers.HandleAppDeploy())
r.Get("/apps/{id}/deployments", s.handlers.HandleAppDeployments()) r.Get("/apps/{id}/deployments", s.handlers.HandleAppDeployments())
r.Get("/apps/{id}/deployments/{deploymentID}/logs", s.handlers.HandleDeploymentLogsAPI()) r.Get("/apps/{id}/deployments/{deploymentID}/logs", s.handlers.HandleDeploymentLogsAPI())
r.Get("/apps/{id}/deployments/{deploymentID}/download", s.handlers.HandleDeploymentLogDownload()) r.Get("/apps/{id}/deployments/{deploymentID}/download", s.handlers.HandleDeploymentLogDownload())
r.Get("/apps/{id}/logs", s.handlers.HandleAppLogs()) r.Get("/apps/{id}/logs", s.handlers.HandleAppLogs())
r.Get("/apps/{id}/container-logs", s.handlers.HandleContainerLogsAPI()) r.Get("/apps/{id}/container-logs", s.handlers.HandleContainerLogsAPI())
r.Get("/apps/{id}/status", s.handlers.HandleAppStatusAPI()) r.Get("/apps/{id}/status", s.handlers.HandleAppStatusAPI())
r.Get("/apps/{id}/recent-deployments", s.handlers.HandleRecentDeploymentsAPI()) r.Get("/apps/{id}/recent-deployments", s.handlers.HandleRecentDeploymentsAPI())
r.Post("/apps/{id}/restart", s.handlers.HandleAppRestart()) r.Post("/apps/{id}/restart", s.handlers.HandleAppRestart())
r.Post("/apps/{id}/stop", s.handlers.HandleAppStop()) r.Post("/apps/{id}/stop", s.handlers.HandleAppStop())
r.Post("/apps/{id}/start", s.handlers.HandleAppStart()) r.Post("/apps/{id}/start", s.handlers.HandleAppStart())
// Environment variables // Environment variables
r.Post("/apps/{id}/env-vars", s.handlers.HandleEnvVarAdd()) r.Post("/apps/{id}/env-vars", s.handlers.HandleEnvVarAdd())
r.Post("/apps/{id}/env-vars/{varID}/delete", s.handlers.HandleEnvVarDelete()) r.Post("/apps/{id}/env-vars/{varID}/delete", s.handlers.HandleEnvVarDelete())
// Labels // Labels
r.Post("/apps/{id}/labels", s.handlers.HandleLabelAdd()) r.Post("/apps/{id}/labels", s.handlers.HandleLabelAdd())
r.Post("/apps/{id}/labels/{labelID}/delete", s.handlers.HandleLabelDelete()) r.Post("/apps/{id}/labels/{labelID}/delete", s.handlers.HandleLabelDelete())
// Volumes // Volumes
r.Post("/apps/{id}/volumes", s.handlers.HandleVolumeAdd()) r.Post("/apps/{id}/volumes", s.handlers.HandleVolumeAdd())
r.Post("/apps/{id}/volumes/{volumeID}/delete", s.handlers.HandleVolumeDelete()) r.Post("/apps/{id}/volumes/{volumeID}/delete", s.handlers.HandleVolumeDelete())
// Ports // Ports
r.Post("/apps/{id}/ports", s.handlers.HandlePortAdd()) r.Post("/apps/{id}/ports", s.handlers.HandlePortAdd())
r.Post("/apps/{id}/ports/{portID}/delete", s.handlers.HandlePortDelete()) r.Post("/apps/{id}/ports/{portID}/delete", s.handlers.HandlePortDelete())
}) })
}) })