rpc: dns rebind protection (#15962)

* cmd,node,rpc: add allowedHosts to prevent dns rebinding attacks

* p2p,node: Fix bug with dumpconfig introduced in r54aeb8e4c0bb9f0e7a6c67258af67df3b266af3d

* rpc: add wildcard support for rpcallowedhosts + go fmt

* cmd/geth, cmd/utils, node, rpc: ignore direct ip(v4/6) addresses in rpc virtual hostnames check

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

* node: change log messages to use geth standard (not sprintf)

* rpc: fix spelling
This commit is contained in:
Martin Holst Swende 2018-02-12 13:52:07 +01:00 committed by Péter Szilágyi
parent 9123eceb0f
commit 589b603a9b
8 changed files with 98 additions and 22 deletions

View File

@ -114,6 +114,7 @@ var (
utils.VMEnableDebugFlag, utils.VMEnableDebugFlag,
utils.NetworkIdFlag, utils.NetworkIdFlag,
utils.RPCCORSDomainFlag, utils.RPCCORSDomainFlag,
utils.RPCVirtualHostsFlag,
utils.EthStatsURLFlag, utils.EthStatsURLFlag,
utils.MetricsEnabledFlag, utils.MetricsEnabledFlag,
utils.FakePoWFlag, utils.FakePoWFlag,

View File

@ -156,6 +156,7 @@ var AppHelpFlagGroups = []flagGroup{
utils.IPCDisabledFlag, utils.IPCDisabledFlag,
utils.IPCPathFlag, utils.IPCPathFlag,
utils.RPCCORSDomainFlag, utils.RPCCORSDomainFlag,
utils.RPCVirtualHostsFlag,
utils.JSpathFlag, utils.JSpathFlag,
utils.ExecFlag, utils.ExecFlag,
utils.PreloadJSFlag, utils.PreloadJSFlag,

View File

@ -397,6 +397,11 @@ var (
Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)", Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)",
Value: "", Value: "",
} }
RPCVirtualHostsFlag = cli.StringFlag{
Name: "rpcvhosts",
Usage: "Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard.",
Value: "localhost",
}
RPCApiFlag = cli.StringFlag{ RPCApiFlag = cli.StringFlag{
Name: "rpcapi", Name: "rpcapi",
Usage: "API's offered over the HTTP-RPC interface", Usage: "API's offered over the HTTP-RPC interface",
@ -690,6 +695,8 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) {
if ctx.GlobalIsSet(RPCApiFlag.Name) { if ctx.GlobalIsSet(RPCApiFlag.Name) {
cfg.HTTPModules = splitAndTrim(ctx.GlobalString(RPCApiFlag.Name)) cfg.HTTPModules = splitAndTrim(ctx.GlobalString(RPCApiFlag.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) (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,6 +141,14 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis
} }
} }
allowedVHosts := api.node.config.HTTPVirtualHosts
if vhosts != nil {
allowedVHosts = nil
for _, vhost := range strings.Split(*host, ",") {
allowedVHosts = append(allowedVHosts, strings.TrimSpace(vhost))
}
}
modules := api.node.httpWhitelist modules := api.node.httpWhitelist
if apis != nil { if apis != nil {
modules = 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, allowedVHosts); err != nil {
return false, err return false, err
} }
return true, nil return true, nil

View File

@ -105,6 +105,15 @@ type Config struct {
// useless for custom HTTP clients. // useless for custom HTTP clients.
HTTPCors []string `toml:",omitempty"` HTTPCors []string `toml:",omitempty"`
// HTTPVirtualHosts is the list of virtual hostnames which are allowed on incoming requests.
// This is by default {'localhost'}. 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.
// Requests using ip address directly are not affected
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
// exposed. // exposed.
@ -137,7 +146,7 @@ type Config struct {
WSExposeAll bool `toml:",omitempty"` WSExposeAll bool `toml:",omitempty"`
// Logger is a custom logger to use with the p2p.Server. // Logger is a custom logger to use with the p2p.Server.
Logger log.Logger Logger log.Logger `toml:",omitempty"`
} }
// IPCEndpoint resolves an IPC endpoint based on a configured value, taking into // IPCEndpoint resolves an IPC endpoint based on a configured value, taking into

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); 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
@ -287,7 +287,7 @@ func (n *Node) startInProc(apis []rpc.API) error {
if err := handler.RegisterName(api.Namespace, api.Service); err != nil { if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
return err return err
} }
n.log.Debug(fmt.Sprintf("InProc registered %T under '%s'", api.Service, api.Namespace)) n.log.Debug("InProc registered", "service", api.Service, "namespace", api.Namespace)
} }
n.inprocHandler = handler n.inprocHandler = handler
return nil return nil
@ -313,7 +313,7 @@ func (n *Node) startIPC(apis []rpc.API) error {
if err := handler.RegisterName(api.Namespace, api.Service); err != nil { if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
return err return err
} }
n.log.Debug(fmt.Sprintf("IPC registered %T under '%s'", api.Service, api.Namespace)) n.log.Debug("IPC registered", "service", api.Service, "namespace", api.Namespace)
} }
// All APIs registered, start the IPC listener // All APIs registered, start the IPC listener
var ( var (
@ -324,7 +324,7 @@ func (n *Node) startIPC(apis []rpc.API) error {
return err return err
} }
go func() { go func() {
n.log.Info(fmt.Sprintf("IPC endpoint opened: %s", n.ipcEndpoint)) n.log.Info("IPC endpoint opened", "url", fmt.Sprintf("%s", n.ipcEndpoint))
for { for {
conn, err := listener.Accept() conn, err := listener.Accept()
@ -337,7 +337,7 @@ func (n *Node) startIPC(apis []rpc.API) error {
return return
} }
// Not closed, just some error; report and continue // Not closed, just some error; report and continue
n.log.Error(fmt.Sprintf("IPC accept failed: %v", err)) n.log.Error("IPC accept failed", "err", err)
continue continue
} }
go handler.ServeCodec(rpc.NewJSONCodec(conn), rpc.OptionMethodInvocation|rpc.OptionSubscriptions) go handler.ServeCodec(rpc.NewJSONCodec(conn), rpc.OptionMethodInvocation|rpc.OptionSubscriptions)
@ -356,7 +356,7 @@ func (n *Node) stopIPC() {
n.ipcListener.Close() n.ipcListener.Close()
n.ipcListener = nil n.ipcListener = nil
n.log.Info(fmt.Sprintf("IPC endpoint closed: %s", n.ipcEndpoint)) n.log.Info("IPC endpoint closed", "endpoint", n.ipcEndpoint)
} }
if n.ipcHandler != nil { if n.ipcHandler != nil {
n.ipcHandler.Stop() n.ipcHandler.Stop()
@ -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) 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
@ -382,7 +382,7 @@ func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors
if err := handler.RegisterName(api.Namespace, api.Service); err != nil { if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
return err return err
} }
n.log.Debug(fmt.Sprintf("HTTP registered %T under '%s'", api.Service, api.Namespace)) n.log.Debug("HTTP registered", "service", api.Service, "namespace", api.Namespace)
} }
} }
// All APIs registered, start the HTTP listener // All APIs registered, start the HTTP listener
@ -393,9 +393,8 @@ 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, 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("HTTP endpoint opened", "url", fmt.Sprintf("http://%s", endpoint), "cors", strings.Join(cors, ","), "hvosts", strings.Join(vhosts, ","))
// All listeners booted successfully // All listeners booted successfully
n.httpEndpoint = endpoint n.httpEndpoint = endpoint
n.httpListener = listener n.httpListener = listener
@ -410,7 +409,7 @@ func (n *Node) stopHTTP() {
n.httpListener.Close() n.httpListener.Close()
n.httpListener = nil n.httpListener = nil
n.log.Info(fmt.Sprintf("HTTP endpoint closed: http://%s", n.httpEndpoint)) n.log.Info("HTTP endpoint closed", "url", fmt.Sprintf("http://%s", n.httpEndpoint))
} }
if n.httpHandler != nil { if n.httpHandler != nil {
n.httpHandler.Stop() n.httpHandler.Stop()
@ -436,7 +435,7 @@ func (n *Node) startWS(endpoint string, apis []rpc.API, modules []string, wsOrig
if err := handler.RegisterName(api.Namespace, api.Service); err != nil { if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
return err return err
} }
n.log.Debug(fmt.Sprintf("WebSocket registered %T under '%s'", api.Service, api.Namespace)) n.log.Debug("WebSocket registered", "service", api.Service, "namespace", api.Namespace)
} }
} }
// All APIs registered, start the HTTP listener // All APIs registered, start the HTTP listener
@ -448,7 +447,7 @@ func (n *Node) startWS(endpoint string, apis []rpc.API, modules []string, wsOrig
return err return err
} }
go rpc.NewWSServer(wsOrigins, handler).Serve(listener) go rpc.NewWSServer(wsOrigins, handler).Serve(listener)
n.log.Info(fmt.Sprintf("WebSocket endpoint opened: ws://%s", listener.Addr())) n.log.Info("WebSocket endpoint opened", "url", fmt.Sprintf("ws://%s", listener.Addr()))
// All listeners booted successfully // All listeners booted successfully
n.wsEndpoint = endpoint n.wsEndpoint = endpoint
@ -464,7 +463,7 @@ func (n *Node) stopWS() {
n.wsListener.Close() n.wsListener.Close()
n.wsListener = nil n.wsListener = nil
n.log.Info(fmt.Sprintf("WebSocket endpoint closed: ws://%s", n.wsEndpoint)) n.log.Info("WebSocket endpoint closed", "url", fmt.Sprintf("ws://%s", n.wsEndpoint))
} }
if n.wsHandler != nil { if n.wsHandler != nil {
n.wsHandler.Stop() n.wsHandler.Stop()

View File

@ -142,7 +142,7 @@ type Config struct {
EnableMsgEvents bool EnableMsgEvents bool
// Logger is a custom logger to use with the p2p.Server. // Logger is a custom logger to use with the p2p.Server.
Logger log.Logger Logger log.Logger `toml:",omitempty"`
} }
// Server manages all peer connections. // Server manages all peer connections.

View File

@ -31,6 +31,7 @@ import (
"time" "time"
"github.com/rs/cors" "github.com/rs/cors"
"strings"
) )
const ( const (
@ -148,8 +149,11 @@ 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, srv *Server) *http.Server { func NewHTTPServer(cors []string, vhosts []string, srv *Server) *http.Server {
return &http.Server{Handler: newCorsHandler(srv, cors)} // Wrap the CORS-handler within a host-handler
handler := newCorsHandler(srv, cors)
handler = newVHostHandler(vhosts, handler)
return &http.Server{Handler: handler}
} }
// ServeHTTP serves JSON-RPC requests over HTTP. // ServeHTTP serves JSON-RPC requests over HTTP.
@ -195,7 +199,6 @@ 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},
@ -204,3 +207,51 @@ func newCorsHandler(srv *Server, allowedOrigins []string) http.Handler {
}) })
return c.Handler(srv) return c.Handler(srv)
} }
// virtualHostHandler is a handler which validates the Host-header of incoming requests.
// The virtualHostHandler 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 virtualHostHandler struct {
vhosts map[string]struct{}
next http.Handler
}
// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler
func (h *virtualHostHandler) 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 == "" {
h.next.ServeHTTP(w, r)
return
}
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
// Either invalid (too many colons) or no port specified
host = r.Host
}
if ipAddr := net.ParseIP(host); ipAddr != nil {
// It's an IP address, we can serve that
h.next.ServeHTTP(w, r)
return
}
// Not an ip address, but a hostname. Need to validate
if _, exist := h.vhosts["*"]; exist {
h.next.ServeHTTP(w, r)
return
}
if _, exist := h.vhosts[host]; exist {
h.next.ServeHTTP(w, r)
return
}
http.Error(w, "invalid host specified", http.StatusForbidden)
return
}
func newVHostHandler(vhosts []string, next http.Handler) http.Handler {
vhostMap := make(map[string]struct{})
for _, allowedHost := range vhosts {
vhostMap[strings.ToLower(allowedHost)] = struct{}{}
}
return &virtualHostHandler{vhostMap, next}
}