fix(cron): switch manual trigger to POST to defeat CSRF

GHSA-8qhj-4f8c-j8qg: GET /api/v1/cron/:id/manual changed shared state
on the agent stream, and the JWT cookie is SameSite=Lax so a victim's
browser would send the cookie on a top-level cross-site GET. An
attacker could trick a logged-in user into firing any of their own
cron commands.

Switch the route to POST: SameSite=Lax cookies are not sent on cross-
site POST, closing the CSRF window without introducing a new token.

Tests:
- TestCronManualTriggerRejectsCrossSiteGET locks in that GET no longer
  resolves.
- TestCronManualTriggerAcceptsSameSitePOST locks in the legitimate POST
  still works.

Frontend (admin-frontend) updated in a follow-up commit.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
This commit is contained in:
naiba
2026-05-26 03:59:50 +00:00
co-authored by cloudcode
parent e05700c2f0
commit 9c0fa84c95
3 changed files with 107 additions and 2 deletions
+1 -1
View File
@@ -135,7 +135,7 @@ func routers(r *gin.Engine, frontendDist fs.FS) {
auth.GET("/cron", listHandler(listCron))
auth.POST("/cron", commonHandler(createCron))
auth.PATCH("/cron/:id", commonHandler(updateCron))
auth.GET("/cron/:id/manual", commonHandler(manualTriggerCron))
auth.POST("/cron/:id/manual", commonHandler(manualTriggerCron))
auth.POST("/batch-delete/cron", commonHandler(batchDeleteCron))
auth.GET("/ddns", listHandler(listDDNS))