mirror of
https://github.com/m1k1o/neko.git
synced 2026-11-03 00:11:48 -06:00
server: reject cross-origin WebSocket handshakes when CORS is empty.
With an empty server.cors, AllowOrigin accepted every Origin, so a hostile page could open an authenticated WebSocket using the SameSite=None session cookie. Now only same-origin, listed origins, or '*' are accepted. Discovered by Yves Soete of Blacksight LLC.
This commit is contained in:
@@ -62,7 +62,7 @@ func (Server) Init(cmd *cobra.Command) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.PersistentFlags().StringSlice("server.cors", []string{}, "list of allowed origins for CORS, if empty CORS is disabled, if '*' is present all origins are allowed")
|
cmd.PersistentFlags().StringSlice("server.cors", []string{}, "list of allowed origins for CORS and WebSocket connections, if empty only same-origin WebSocket connections are accepted, if '*' is present all origins are allowed")
|
||||||
if err := viper.BindPFlag("server.cors", cmd.PersistentFlags().Lookup("server.cors")); err != nil {
|
if err := viper.BindPFlag("server.cors", cmd.PersistentFlags().Lookup("server.cors")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -193,9 +193,8 @@ func (s *Server) HasCors() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) AllowOrigin(origin string) bool {
|
func (s *Server) AllowOrigin(origin string) bool {
|
||||||
// if CORS is not configured, or a wildcard is present,
|
// only an explicit wildcard allows any origin
|
||||||
// allow WebSocket connections from any origin
|
if len(s.CORS) > 0 && s.CORS[0] == "*" {
|
||||||
if len(s.CORS) == 0 || s.CORS[0] == "*" {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package http
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
@@ -53,7 +55,13 @@ func New(WebSocketManager types.WebSocketManager, ApiManager types.ApiManager, c
|
|||||||
|
|
||||||
router.Get("/api/ws", WebSocketManager.Upgrade(func(r *http.Request) bool {
|
router.Get("/api/ws", WebSocketManager.Upgrade(func(r *http.Request) bool {
|
||||||
origin := r.Header.Get("Origin")
|
origin := r.Header.Get("Origin")
|
||||||
return origin == "" || config.AllowOrigin(origin)
|
if origin == "" || config.AllowOrigin(origin) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// by default, only same-origin browser connections are accepted
|
||||||
|
u, err := url.Parse(origin)
|
||||||
|
return err == nil && u.Host != "" && strings.EqualFold(u.Host, r.Host)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
batch := batchHandler{
|
batch := batchHandler{
|
||||||
|
|||||||
@@ -109,3 +109,24 @@ func TestWebSocketOriginAllowsInternalDialWithoutOrigin(t *testing.T) {
|
|||||||
t.Fatal("unconfigured browser Origin was accepted")
|
t.Fatal("unconfigured browser Origin was accepted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebSocketOriginDefaultsToSameOrigin(t *testing.T) {
|
||||||
|
webSocketManager := &originCapturingWebSocketManager{}
|
||||||
|
serverConfig := &config.Server{}
|
||||||
|
memberConfig := &config.Member{}
|
||||||
|
sessionManager := session.New(&config.Session{})
|
||||||
|
apiManager := api.New(sessionManager, member.New(sessionManager, memberConfig), nil, nil, memberConfig, serverConfig)
|
||||||
|
New(webSocketManager, apiManager, serverConfig, memberConfig)
|
||||||
|
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "http://neko.example.test/api/ws", nil)
|
||||||
|
|
||||||
|
request.Header.Set("Origin", "http://neko.example.test")
|
||||||
|
if !webSocketManager.checkOrigin(request) {
|
||||||
|
t.Fatal("same Origin was rejected")
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set("Origin", "https://attacker.example")
|
||||||
|
if webSocketManager.checkOrigin(request) {
|
||||||
|
t.Fatal("cross-site Origin was accepted with empty CORS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
--plugins.required if true, neko will exit if there is an error when loading a plugin
|
--plugins.required if true, neko will exit if there is an error when loading a plugin
|
||||||
--server.bind string address/port/socket to serve neko (default "127.0.0.1:8080")
|
--server.bind string address/port/socket to serve neko (default "127.0.0.1:8080")
|
||||||
--server.cert string path to the SSL cert used to secure the neko server
|
--server.cert string path to the SSL cert used to secure the neko server
|
||||||
--server.cors strings list of allowed origins for CORS, if empty CORS is disabled, if '*' is present all origins are allowed
|
--server.cors strings list of allowed origins for CORS and WebSocket connections, if empty only same-origin WebSocket connections are accepted, if '*' is present all origins are allowed
|
||||||
--server.key string path to the SSL key used to secure the neko server
|
--server.key string path to the SSL key used to secure the neko server
|
||||||
--server.metrics enable prometheus metrics available at /metrics (default true)
|
--server.metrics enable prometheus metrics available at /metrics (default true)
|
||||||
--server.path_prefix string path prefix for HTTP requests (default "/")
|
--server.path_prefix string path prefix for HTTP requests (default "/")
|
||||||
|
|||||||
Reference in New Issue
Block a user