diff --git a/cmd/geth/main.go b/cmd/geth/main.go index b955bd243e..6d690a584e 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -111,6 +111,7 @@ var ( utils.VMEnableDebugFlag, utils.NetworkIdFlag, utils.RPCCORSDomainFlag, + utils.RPCAllowedHostsFlag, utils.EthStatsURLFlag, utils.MetricsEnabledFlag, utils.FakePoWFlag, diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index a834d5b7ae..ad00596216 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -152,6 +152,7 @@ var AppHelpFlagGroups = []flagGroup{ utils.IPCDisabledFlag, utils.IPCPathFlag, utils.RPCCORSDomainFlag, + utils.RPCAllowedHostsFlag, utils.JSpathFlag, utils.ExecFlag, utils.PreloadJSFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 89dcd230c4..117030b20c 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -383,6 +383,11 @@ var ( Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)", Value: "", } + RPCAllowedHostsFlag = cli.StringFlag{ + Name: "rpcallowedhosts", + Usage: "Comma separated list of hostnames from which to accept requests (server enforced)", + Value: "localhost,127.0.0.1", + } RPCApiFlag = cli.StringFlag{ Name: "rpcapi", Usage: "API's offered over the HTTP-RPC interface", @@ -676,6 +681,8 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) { if ctx.GlobalIsSet(RPCApiFlag.Name) { cfg.HTTPModules = splitAndTrim(ctx.GlobalString(RPCApiFlag.Name)) } + + cfg.HTTPHosts = splitAndTrim(ctx.GlobalString(RPCAllowedHostsFlag.Name)) } // setWS creates the WebSocket RPC listener interface string from the set diff --git a/node/api.go b/node/api.go index 1b04b70938..3cd54b06a6 100644 --- a/node/api.go +++ b/node/api.go @@ -114,7 +114,7 @@ func (api *PrivateAdminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription, } // StartRPC starts the HTTP RPC API server. -func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis *string) (bool, error) { +func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis *string, hosts *string) (bool, error) { api.node.lock.Lock() defer api.node.lock.Unlock() @@ -141,6 +141,14 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis } } + allowedHosts := api.node.config.HTTPHosts + if hosts != nil{ + allowedHosts = nil + for _, host := range strings.Split(*host, ",") { + allowedHosts = append(allowedHosts, strings.TrimSpace(host)) + } + } + modules := api.node.httpWhitelist if apis != nil { modules = nil @@ -149,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); err != nil { + if err := api.node.startHTTP(fmt.Sprintf("%s:%d", *host, *port), api.node.rpcAPIs, modules, allowedOrigins, allowedHosts); err != nil { return false, err } return true, nil diff --git a/node/config.go b/node/config.go index 7a0c1688ec..0b51250fa3 100644 --- a/node/config.go +++ b/node/config.go @@ -105,6 +105,14 @@ type Config struct { // useless for custom HTTP clients. HTTPCors []string `toml:",omitempty"` + // HTTPHosts is the list of hosts which are allowed on incoming requests. + // This is by default {'localhost','127.0.0.1'}. Using this prevents attacks like + // 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. + // By explicitly checking the Host-header, the server will not allow requests + // made against the server with a malicious host domain. + HTTPHosts []string `toml:",omitempty"` + // 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 // exposed. diff --git a/node/node.go b/node/node.go index ff7258033e..6ae6dce1fc 100644 --- a/node/node.go +++ b/node/node.go @@ -263,7 +263,7 @@ func (n *Node) startRPC(services map[reflect.Type]Service) error { n.stopInProc() return err } - if err := n.startHTTP(n.httpEndpoint, apis, n.config.HTTPModules, n.config.HTTPCors); err != nil { + if err := n.startHTTP(n.httpEndpoint, apis, n.config.HTTPModules, n.config.HTTPCors, n.config.HTTPHosts); err != nil { n.stopIPC() n.stopInProc() return err @@ -365,7 +365,7 @@ func (n *Node) stopIPC() { } // startHTTP initializes and starts the HTTP RPC endpoint. -func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors []string) error { +func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors []string, allowedHosts []string) error { // Short circuit if the HTTP endpoint isn't being exposed if endpoint == "" { 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 { return err } - go rpc.NewHTTPServer(cors, handler).Serve(listener) + go rpc.NewHTTPServer(cors, allowedHosts, handler).Serve(listener) n.log.Info(fmt.Sprintf("HTTP endpoint opened: http://%s", endpoint)) - + n.log.Info(fmt.Sprintf("HTTP config: cors %v, hosts%v", cors,allowedHosts)) // All listeners booted successfully n.httpEndpoint = endpoint n.httpListener = listener diff --git a/rpc/http.go b/rpc/http.go index d61b0e470b..c96f1842bd 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -31,6 +31,7 @@ import ( "time" "github.com/rs/cors" + "strings" ) const ( @@ -142,8 +143,11 @@ func (t *httpReadWriteNopCloser) Close() error { // NewHTTPServer creates a new HTTP RPC server around an API provider. // // Deprecated: Server implements http.Handler -func NewHTTPServer(cors []string, srv *Server) *http.Server { - return &http.Server{Handler: newCorsHandler(srv, cors)} +func NewHTTPServer(cors []string, hosts[] string, srv *Server) *http.Server { + // Wrap the CORS-handler within a host-handler + handler := newCorsHandler(srv, cors) + handler = newHostHandler(hosts, handler) + return &http.Server{Handler: handler} } // ServeHTTP serves JSON-RPC requests over HTTP. @@ -166,6 +170,10 @@ func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { srv.ServeSingleRequest(codec, OptionMethodInvocation) } +func parseHost(hostport string) string { + return strings.ToLower(strings.Split(hostport, ":")[0]) +} + // validateRequest returns a non-zero response code and error message if the // request is invalid. func validateRequest(r *http.Request) (int, error) { @@ -195,6 +203,39 @@ func newCorsHandler(srv *Server, allowedOrigins []string) http.Handler { AllowedMethods: []string{http.MethodPost, http.MethodGet}, MaxAge: 600, AllowedHeaders: []string{"*"}, + Debug: true, }) return c.Handler(srv) } + +// hostHandler is a handler which validates the Host-header of incoming requests. +// The hostHandler 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 +// which domain was used, and validate that against a whitelist. +type hostHandler struct { + AllowedHosts []string + next http.Handler +} + +// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler +func (h *hostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + + if r.Host != "" { + hostpart := parseHost(r.Host) + requestAllowed := false + for _, allowedHost := range h.AllowedHosts { + if strings.ToLower(allowedHost) == hostpart { + requestAllowed = true + } + } + if !requestAllowed { + http.Error(w, "invalid host specified", http.StatusForbidden) + return + } + } + h.next.ServeHTTP(w, r) +} + +func newHostHandler(allowedHosts []string, next http.Handler) http.Handler { + return &hostHandler{allowedHosts, next} +}