c4a856dc10
* fix(backend): reject disallowed cross-origin state changes in CORS The CORS middleware only aborted disallowed origins on the OPTIONS preflight, then called c.Next() for every other method. A CORS-safelisted request (e.g. a multipart/form-data POST, which skips preflight) from a malicious page therefore still executed the handler's side effect even though the browser could not read the response. Against a loopback-bound backend this is a drive-by CSRF primitive: any web page the victim visits could POST to a mutating endpoint such as /api/plugins/install, which unpacks and spawns the uploaded plugin binary (RCE), with no authentication layer in front of it. Now a disallowed cross-origin request is rejected with 403 for the preflight and every state-changing method; only CORS-safe methods (GET/HEAD) fall through without CORS headers, so the browser still cannot read the response and no state is changed. No-Origin (CLI/curl) and allowed same/loopback-origin requests are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(backend): block all disallowed cross-origin requests in CORS Addresses review: GET is not universally side-effect-free here. Plugin webhooks are registered for GET as well as POST (/api/plugins/:id/webhooks/:key -> Service.InvokeWebhook), so the earlier GET/HEAD whitelist still let a disallowed-origin browser GET trigger that unauthenticated side effect via drive-by CSRF. Reject every method (including GET/HEAD) with 403 for a disallowed origin. This loses nothing legitimate: a response the browser could read cross-origin needs CORS headers we only emit for allowed origins. Non-browser clients (CLI/curl) and resource loads (<img>/<script>) send no Origin and take the wildcard branch, so they are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
1.8 KiB
Go
50 lines
1.8 KiB
Go
package backendapp
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/kandev/kandev/internal/common/httpmw"
|
|
)
|
|
|
|
// corsMiddleware returns a CORS middleware for HTTP and WebSocket connections.
|
|
func corsMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if origin := c.Request.Header.Get("Origin"); origin != "" {
|
|
if !httpmw.AllowedOrigin(origin, c.Request.Host) {
|
|
// Disallowed cross-origin browser request: reject every method
|
|
// (including GET/HEAD) with 403. Letting one reach the handler
|
|
// yields nothing legitimate — a response the browser could read
|
|
// cross-origin needs CORS headers we only emit for allowed
|
|
// origins — but it does expose side-effecting endpoints to
|
|
// drive-by CSRF: a browser "simple request" (e.g. a
|
|
// multipart/form-data POST, CORS-safelisted so it skips
|
|
// preflight) or even a plain GET (plugin webhooks are
|
|
// registered for GET as well as POST, see plugins/handlers.go)
|
|
// would still trigger the side effect even though the page
|
|
// can't read the response. Non-browser clients (CLI/curl) and
|
|
// resource loads (<img>/<script>) send no Origin and take the
|
|
// branch below, so this doesn't break them.
|
|
c.AbortWithStatus(http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
c.Header("Access-Control-Allow-Origin", origin)
|
|
c.Header("Vary", "Origin")
|
|
c.Header("Access-Control-Allow-Credentials", "true")
|
|
} else {
|
|
c.Header("Access-Control-Allow-Origin", "*")
|
|
}
|
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS")
|
|
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization, Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Protocol")
|
|
|
|
if c.Request.Method == "OPTIONS" {
|
|
c.AbortWithStatus(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|