http, rpc, utils: make vhosts into map, address review concerns

This commit is contained in:
Martin Holst Swende 2018-02-05 15:52:56 +01:00
parent 543df3f69c
commit dbf5bb1f4c
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
5 changed files with 36 additions and 33 deletions

View file

@ -385,7 +385,7 @@ var (
} }
RPCVirtualHostsFlag = cli.StringFlag{ RPCVirtualHostsFlag = cli.StringFlag{
Name: "rpcvhosts", Name: "rpcvhosts",
Usage: "Comma separated list of virtual hostnames from which to accept requests (server enforced). Set to * to disable this protection.", Usage: "Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard.",
Value: "localhost", Value: "localhost",
} }
RPCApiFlag = cli.StringFlag{ RPCApiFlag = cli.StringFlag{
@ -682,7 +682,7 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) {
cfg.HTTPModules = splitAndTrim(ctx.GlobalString(RPCApiFlag.Name)) cfg.HTTPModules = splitAndTrim(ctx.GlobalString(RPCApiFlag.Name))
} }
cfg.HTTPVirtualHostnames = splitAndTrim(ctx.GlobalString(RPCVirtualHostsFlag.Name)) cfg.HTTPVirtualHosts = splitAndTrim(ctx.GlobalString(RPCVirtualHostsFlag.Name))
} }
// setWS creates the WebSocket RPC listener interface string from the set // setWS creates the WebSocket RPC listener interface string from the set

View file

@ -114,7 +114,7 @@ func (api *PrivateAdminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription,
} }
// StartRPC starts the HTTP RPC API server. // StartRPC starts the HTTP RPC API server.
func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis *string, hosts *string) (bool, error) { func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) {
api.node.lock.Lock() api.node.lock.Lock()
defer api.node.lock.Unlock() defer api.node.lock.Unlock()
@ -141,11 +141,11 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis
} }
} }
allowedHosts := api.node.config.HTTPVirtualHostnames allowedVHosts := api.node.config.HTTPVirtualHosts
if hosts != nil { if vhosts != nil {
allowedHosts = nil allowedVHosts = nil
for _, host := range strings.Split(*host, ",") { for _, vhost := range strings.Split(*host, ",") {
allowedHosts = append(allowedHosts, strings.TrimSpace(host)) allowedVHosts = append(allowedVHosts, strings.TrimSpace(vhost))
} }
} }
@ -157,7 +157,7 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis
} }
} }
if err := api.node.startHTTP(fmt.Sprintf("%s:%d", *host, *port), api.node.rpcAPIs, modules, allowedOrigins, allowedHosts); err != nil { if err := api.node.startHTTP(fmt.Sprintf("%s:%d", *host, *port), api.node.rpcAPIs, modules, allowedOrigins, allowedVHosts); err != nil {
return false, err return false, err
} }
return true, nil return true, nil

View file

@ -105,14 +105,14 @@ type Config struct {
// useless for custom HTTP clients. // useless for custom HTTP clients.
HTTPCors []string `toml:",omitempty"` HTTPCors []string `toml:",omitempty"`
// HTTPVirtualHostnames is the list of virtual hostnames which are allowed on incoming requests. // HTTPVirtualHosts is the list of virtual hostnames which are allowed on incoming requests.
// This is by default {'localhost'}. Using this prevents attacks like // This is by default {'localhost'}. Using this prevents attacks like
// DNS rebinding, which bypasses SOP by simply masquerading as being within the same // DNS rebinding, which bypasses SOP by simply masquerading as being within the same
// origin. These attacks do not utilize CORS, since they are not cross-domain. // origin. These attacks do not utilize CORS, since they are not cross-domain.
// By explicitly checking the Host-header, the server will not allow requests // By explicitly checking the Host-header, the server will not allow requests
// made against the server with a malicious host domain. // made against the server with a malicious host domain.
// Requests using ip address directly are not affected // Requests using ip address directly are not affected
HTTPVirtualHostnames []string `toml:",omitempty"` HTTPVirtualHosts []string `toml:",omitempty"`
// HTTPModules is a list of API modules to expose via the HTTP RPC interface. // HTTPModules is a list of API modules to expose via the HTTP RPC interface.
// If the module list is empty, all RPC API endpoints designated public will be // If the module list is empty, all RPC API endpoints designated public will be

View file

@ -263,7 +263,7 @@ func (n *Node) startRPC(services map[reflect.Type]Service) error {
n.stopInProc() n.stopInProc()
return err return err
} }
if err := n.startHTTP(n.httpEndpoint, apis, n.config.HTTPModules, n.config.HTTPCors, n.config.HTTPVirtualHostnames); err != nil { if err := n.startHTTP(n.httpEndpoint, apis, n.config.HTTPModules, n.config.HTTPCors, n.config.HTTPVirtualHosts); err != nil {
n.stopIPC() n.stopIPC()
n.stopInProc() n.stopInProc()
return err return err
@ -365,7 +365,7 @@ func (n *Node) stopIPC() {
} }
// startHTTP initializes and starts the HTTP RPC endpoint. // startHTTP initializes and starts the HTTP RPC endpoint.
func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors []string, allowedHosts []string) error { func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors []string, vhosts []string) error {
// Short circuit if the HTTP endpoint isn't being exposed // Short circuit if the HTTP endpoint isn't being exposed
if endpoint == "" { if endpoint == "" {
return nil return nil
@ -393,9 +393,9 @@ func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors
if listener, err = net.Listen("tcp", endpoint); err != nil { if listener, err = net.Listen("tcp", endpoint); err != nil {
return err return err
} }
go rpc.NewHTTPServer(cors, allowedHosts, handler).Serve(listener) go rpc.NewHTTPServer(cors, vhosts, handler).Serve(listener)
n.log.Info(fmt.Sprintf("HTTP endpoint opened: http://%s", endpoint)) n.log.Info(fmt.Sprintf("HTTP endpoint opened: http://%s", endpoint))
n.log.Info(fmt.Sprintf("HTTP config: cors %v, vhosts%v", cors, allowedHosts)) n.log.Info(fmt.Sprintf("HTTP config: cors %v, vhosts%v", cors, vhosts))
// All listeners booted successfully // All listeners booted successfully
n.httpEndpoint = endpoint n.httpEndpoint = endpoint
n.httpListener = listener n.httpListener = listener

View file

@ -143,10 +143,10 @@ func (t *httpReadWriteNopCloser) Close() error {
// NewHTTPServer creates a new HTTP RPC server around an API provider. // NewHTTPServer creates a new HTTP RPC server around an API provider.
// //
// Deprecated: Server implements http.Handler // Deprecated: Server implements http.Handler
func NewHTTPServer(cors []string, hosts []string, srv *Server) *http.Server { func NewHTTPServer(cors []string, vhosts []string, srv *Server) *http.Server {
// Wrap the CORS-handler within a host-handler // Wrap the CORS-handler within a host-handler
handler := newCorsHandler(srv, cors) handler := newCorsHandler(srv, cors)
handler = newHostHandler(hosts, handler) handler = newVHostHandler(vhosts, handler)
return &http.Server{Handler: handler} return &http.Server{Handler: handler}
} }
@ -193,29 +193,26 @@ func newCorsHandler(srv *Server, allowedOrigins []string) http.Handler {
if len(allowedOrigins) == 0 { if len(allowedOrigins) == 0 {
return srv return srv
} }
c := cors.New(cors.Options{ c := cors.New(cors.Options{
AllowedOrigins: allowedOrigins, AllowedOrigins: allowedOrigins,
AllowedMethods: []string{http.MethodPost, http.MethodGet}, AllowedMethods: []string{http.MethodPost, http.MethodGet},
MaxAge: 600, MaxAge: 600,
AllowedHeaders: []string{"*"}, AllowedHeaders: []string{"*"},
Debug: true,
}) })
return c.Handler(srv) return c.Handler(srv)
} }
// hostHandler is a handler which validates the Host-header of incoming requests. // virtalHostHandler is a handler which validates the Host-header of incoming requests.
// The hostHandler can prevent DNS rebinding attacks, which do not utilize CORS-headers, // The virtalHostHandler can prevent DNS rebinding attacks, which do not utilize CORS-headers,
// since they do in-domain requests against the RPC api. Instead, we can see on the Host-header // since they do in-domain requests against the RPC api. Instead, we can see on the Host-header
// which domain was used, and validate that against a whitelist. // which domain was used, and validate that against a whitelist.
type hostHandler struct { type virtalHostHandler struct {
AllowedHosts []string vhosts map[string]struct{}
next http.Handler next http.Handler
} }
// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler // ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler
func (h *hostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *virtalHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// if r.Host is not set, we can continue serving since a browser would set the Host header // if r.Host is not set, we can continue serving since a browser would set the Host header
if r.Host == "" { if r.Host == "" {
h.next.ServeHTTP(w, r) h.next.ServeHTTP(w, r)
@ -233,16 +230,22 @@ func (h *hostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
// Not an ip address, but a hostname. Need to validate // Not an ip address, but a hostname. Need to validate
for _, allowedHost := range h.AllowedHosts { if _, exist := h.vhosts["*"]; exist {
if strings.ToLower(allowedHost) == strings.ToLower(host) || allowedHost == "*" { h.next.ServeHTTP(w, r)
h.next.ServeHTTP(w, r) return
return }
} if _, exist := h.vhosts[host]; exist {
h.next.ServeHTTP(w, r)
return
} }
http.Error(w, "invalid host specified", http.StatusForbidden) http.Error(w, "invalid host specified", http.StatusForbidden)
return return
} }
func newHostHandler(allowedHosts []string, next http.Handler) http.Handler { func newVHostHandler(vhosts []string, next http.Handler) http.Handler {
return &hostHandler{allowedHosts, next} vhostMap := make(map[string]struct{})
for _, allowedHost := range vhosts {
vhostMap[strings.ToLower(allowedHost)] = struct{}{}
}
return &virtalHostHandler{vhostMap, next}
} }