fix(http): drop the global CSRF relaxation and CORS-derived socket origin

CSRFMiddlewareWithCORS changed CSRF handling for every route and, with a
wildcard allowlist, disabled the Sec-Fetch-Site check. Restore the
original CSRFMiddleware: clients that authenticate with validated header
credentials, or with a single-use WebSocket ticket, are already exempt,
so the motion routes need no relaxation.

The motion WebSocket upgrade no longer reads the CORS allowlist to
decide whether a cross-origin upgrade is acceptable. It accepts one only
with a valid ticket or validated header credentials; everything else
gets the same-origin default.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-10-13 08:27:24 +00:00
1 parent db929f4a08
commit e7687e9fe9
3 files changed
+13 -35

No files matched your search

+1 -2
View File
@@ -458,8 +458,7 @@ func API(application *application.Application) (*echo.Echo, error) {
// could never read a token to send back.
if !application.ApplicationConfig().DisableCSRF {
xlog.Debug("Enabling CSRF middleware (Sec-Fetch-Site mode)")
e.Use(auth.CSRFMiddlewareWithCORS(
application.ApplicationConfig().CORS && application.ApplicationConfig().CORSAllowOrigins != ""))
e.Use(auth.CSRFMiddleware())
}
// Admin middleware: enforces admin role when auth is enabled, no-op otherwise
+4 -9
View File
@@ -62,7 +62,6 @@ type motionSession struct {
type MotionEndpoints struct {
ingress metric.Int64Counter
app *application.Application
corsEnabled bool
mu sync.Mutex
sessions map[string]*motionSession
reserved map[string]bool
@@ -74,9 +73,6 @@ type MotionEndpoints struct {
func NewMotionEndpoints(app *application.Application) *MotionEndpoints {
meter := otel.Meter("github.com/mudler/LocalAI/motion")
m := &MotionEndpoints{app: app, sessions: map[string]*motionSession{}, reserved: map[string]bool{}}
if app != nil {
m.corsEnabled = app.ApplicationConfig().CORS && app.ApplicationConfig().CORSAllowOrigins != ""
}
m.active, _ = meter.Int64UpDownCounter("localai_motion_sessions", metric.WithDescription("Active motion sessions"))
m.ingress, _ = meter.Int64Counter("localai_motion_uploads", metric.WithDescription("Duplex frames accepted or dropped before inference"))
m.outcomes, _ = meter.Int64Counter("localai_motion_frames", metric.WithDescription("Motion outcomes and rejected input"))
@@ -384,8 +380,9 @@ func (m *MotionEndpoints) Poses(c echo.Context) error {
if err != nil {
return err
}
// Validated header credentials or an explicit CORS policy extend the origin default.
// Reuse middleware approval to keep origin matching consistent with HTTP.
// A non-same-origin upgrade is accepted only with a single-use ticket or
// validated header credentials. The CORS allowlist never relaxes this: a
// cookie-authenticated cross-origin page is rejected by the default check.
duplex := false
for _, protocol := range websocket.Subprotocols(c.Request()) {
if protocol == "localai.motion.v2" {
@@ -400,9 +397,7 @@ func (m *MotionEndpoints) Poses(c echo.Context) error {
}
defer s.frameMu.Unlock()
upgrader := websocket.Upgrader{Subprotocols: []string{"localai.motion.v2"}}
origin := c.Request().Header.Get(echo.HeaderOrigin)
allowedOrigin := c.Response().Header().Get(echo.HeaderAccessControlAllowOrigin)
if auth.WebSocketTicketAuthenticated(c) || auth.HeaderAuthenticated(c) || (m.corsEnabled && origin != "" && (allowedOrigin == "*" || allowedOrigin == origin)) {
if auth.WebSocketTicketAuthenticated(c) || auth.HeaderAuthenticated(c) {
upgrader.CheckOrigin = func(*http.Request) bool { return true }
}
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
@@ -174,36 +174,20 @@ var _ = Describe("Motion streaming endpoints", func() {
Expect(response.StatusCode).To(Equal(403))
Expect(response.Body.Close()).To(Succeed())
})
DescribeTable("honors explicitly enabled CORS for browser connections", func(enabled bool, allowed []string, origin string, accept bool) {
m.corsEnabled = enabled
DescribeTable("never derives the WebSocket origin check from the CORS allowlist", func(allowed []string, origin string) {
e := echo.New()
e.Use(echomiddleware.CORSWithConfig(echomiddleware.CORSConfig{AllowOrigins: allowed}))
e.GET("/api/motion/sessions/:id/poses", m.Poses)
server := httptest.NewServer(e)
DeferCleanup(server.Close)
ws, response, err := (&websocket.Dialer{Subprotocols: []string{"localai.motion.v2"}}).Dial("ws"+strings.TrimPrefix(server.URL, "http")+"/api/motion/sessions/session/poses", http.Header{"Origin": []string{origin}})
if !accept {
Expect(err).To(HaveOccurred())
Expect(response.StatusCode).To(Equal(403))
Expect(response.Body.Close()).To(Succeed())
return
}
Expect(err).NotTo(HaveOccurred())
DeferCleanup(func() { Expect(ws.Close()).To(Succeed()) })
Expect(response.StatusCode).To(Equal(101))
kind, data, err := ws.ReadMessage()
Expect(err).NotTo(HaveOccurred())
Expect(kind).To(Equal(websocket.BinaryMessage))
out := &motion.Output{}
Expect(proto.Unmarshal(data, out)).To(Succeed())
Expect(out.GetDefinition().Profile).To(Equal("smpl24"))
_, response, err := (&websocket.Dialer{Subprotocols: []string{"localai.motion.v2"}}).Dial("ws"+strings.TrimPrefix(server.URL, "http")+"/api/motion/sessions/session/poses", http.Header{"Origin": []string{origin}})
Expect(err).To(HaveOccurred())
Expect(response.StatusCode).To(Equal(403))
Expect(response.Body.Close()).To(Succeed())
},
Entry("explicit allowed origin", true, []string{"https://consumer.example"}, "https://consumer.example", true),
Entry("explicit wildcard policy", true, []string{"*"}, "http://localhost:3000", true),
Entry("subdomain policy", true, []string{"https://*.example.com"}, "https://consumer.example.com", true),
Entry("disallowed origin", true, []string{"https://consumer.example"}, "https://untrusted.example", false),
Entry("different port", true, []string{"http://localhost:3000"}, "http://localhost:3001", false),
Entry("default HTTP wildcard does not enable cross-origin sockets", false, []string{"*"}, "https://untrusted.example", false),
Entry("explicitly allowed origin", []string{"https://consumer.example"}, "https://consumer.example"),
Entry("wildcard policy", []string{"*"}, "http://localhost:3000"),
Entry("subdomain policy", []string{"https://*.example.com"}, "https://consumer.example.com"),
)
DescribeTable("bypasses origins only for validated header credentials", func(header, value string, status int) {