Skip to content

Commit 589b603

Browse files
holimankaralabe
authored andcommitted
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
1 parent 9123ece commit 589b603

File tree

8 files changed

+98
-22
lines changed

8 files changed

+98
-22
lines changed

cmd/geth/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ var (
114114
utils.VMEnableDebugFlag,
115115
utils.NetworkIdFlag,
116116
utils.RPCCORSDomainFlag,
117+
utils.RPCVirtualHostsFlag,
117118
utils.EthStatsURLFlag,
118119
utils.MetricsEnabledFlag,
119120
utils.FakePoWFlag,

cmd/geth/usage.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ var AppHelpFlagGroups = []flagGroup{
156156
utils.IPCDisabledFlag,
157157
utils.IPCPathFlag,
158158
utils.RPCCORSDomainFlag,
159+
utils.RPCVirtualHostsFlag,
159160
utils.JSpathFlag,
160161
utils.ExecFlag,
161162
utils.PreloadJSFlag,

cmd/utils/flags.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,11 @@ var (
397397
Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)",
398398
Value: "",
399399
}
400+
RPCVirtualHostsFlag = cli.StringFlag{
401+
Name: "rpcvhosts",
402+
Usage: "Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard.",
403+
Value: "localhost",
404+
}
400405
RPCApiFlag = cli.StringFlag{
401406
Name: "rpcapi",
402407
Usage: "API's offered over the HTTP-RPC interface",
@@ -690,6 +695,8 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) {
690695
if ctx.GlobalIsSet(RPCApiFlag.Name) {
691696
cfg.HTTPModules = splitAndTrim(ctx.GlobalString(RPCApiFlag.Name))
692697
}
698+
699+
cfg.HTTPVirtualHosts = splitAndTrim(ctx.GlobalString(RPCVirtualHostsFlag.Name))
693700
}
694701

695702
// setWS creates the WebSocket RPC listener interface string from the set

node/api.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func (api *PrivateAdminAPI) PeerEvents(ctx context.Context) (*rpc.Subscription,
114114
}
115115

116116
// StartRPC starts the HTTP RPC API server.
117-
func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis *string) (bool, error) {
117+
func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis *string, vhosts *string) (bool, error) {
118118
api.node.lock.Lock()
119119
defer api.node.lock.Unlock()
120120

@@ -141,6 +141,14 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis
141141
}
142142
}
143143

144+
allowedVHosts := api.node.config.HTTPVirtualHosts
145+
if vhosts != nil {
146+
allowedVHosts = nil
147+
for _, vhost := range strings.Split(*host, ",") {
148+
allowedVHosts = append(allowedVHosts, strings.TrimSpace(vhost))
149+
}
150+
}
151+
144152
modules := api.node.httpWhitelist
145153
if apis != nil {
146154
modules = nil
@@ -149,7 +157,7 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis
149157
}
150158
}
151159

152-
if err := api.node.startHTTP(fmt.Sprintf("%s:%d", *host, *port), api.node.rpcAPIs, modules, allowedOrigins); err != nil {
160+
if err := api.node.startHTTP(fmt.Sprintf("%s:%d", *host, *port), api.node.rpcAPIs, modules, allowedOrigins, allowedVHosts); err != nil {
153161
return false, err
154162
}
155163
return true, nil

node/config.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ type Config struct {
105105
// useless for custom HTTP clients.
106106
HTTPCors []string `toml:",omitempty"`
107107

108+
// HTTPVirtualHosts is the list of virtual hostnames which are allowed on incoming requests.
109+
// This is by default {'localhost'}. Using this prevents attacks like
110+
// DNS rebinding, which bypasses SOP by simply masquerading as being within the same
111+
// origin. These attacks do not utilize CORS, since they are not cross-domain.
112+
// By explicitly checking the Host-header, the server will not allow requests
113+
// made against the server with a malicious host domain.
114+
// Requests using ip address directly are not affected
115+
HTTPVirtualHosts []string `toml:",omitempty"`
116+
108117
// HTTPModules is a list of API modules to expose via the HTTP RPC interface.
109118
// If the module list is empty, all RPC API endpoints designated public will be
110119
// exposed.
@@ -137,7 +146,7 @@ type Config struct {
137146
WSExposeAll bool `toml:",omitempty"`
138147

139148
// Logger is a custom logger to use with the p2p.Server.
140-
Logger log.Logger
149+
Logger log.Logger `toml:",omitempty"`
141150
}
142151

143152
// IPCEndpoint resolves an IPC endpoint based on a configured value, taking into

node/node.go

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ func (n *Node) startRPC(services map[reflect.Type]Service) error {
263263
n.stopInProc()
264264
return err
265265
}
266-
if err := n.startHTTP(n.httpEndpoint, apis, n.config.HTTPModules, n.config.HTTPCors); err != nil {
266+
if err := n.startHTTP(n.httpEndpoint, apis, n.config.HTTPModules, n.config.HTTPCors, n.config.HTTPVirtualHosts); err != nil {
267267
n.stopIPC()
268268
n.stopInProc()
269269
return err
@@ -287,7 +287,7 @@ func (n *Node) startInProc(apis []rpc.API) error {
287287
if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
288288
return err
289289
}
290-
n.log.Debug(fmt.Sprintf("InProc registered %T under '%s'", api.Service, api.Namespace))
290+
n.log.Debug("InProc registered", "service", api.Service, "namespace", api.Namespace)
291291
}
292292
n.inprocHandler = handler
293293
return nil
@@ -313,7 +313,7 @@ func (n *Node) startIPC(apis []rpc.API) error {
313313
if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
314314
return err
315315
}
316-
n.log.Debug(fmt.Sprintf("IPC registered %T under '%s'", api.Service, api.Namespace))
316+
n.log.Debug("IPC registered", "service", api.Service, "namespace", api.Namespace)
317317
}
318318
// All APIs registered, start the IPC listener
319319
var (
@@ -324,7 +324,7 @@ func (n *Node) startIPC(apis []rpc.API) error {
324324
return err
325325
}
326326
go func() {
327-
n.log.Info(fmt.Sprintf("IPC endpoint opened: %s", n.ipcEndpoint))
327+
n.log.Info("IPC endpoint opened", "url", fmt.Sprintf("%s", n.ipcEndpoint))
328328

329329
for {
330330
conn, err := listener.Accept()
@@ -337,7 +337,7 @@ func (n *Node) startIPC(apis []rpc.API) error {
337337
return
338338
}
339339
// Not closed, just some error; report and continue
340-
n.log.Error(fmt.Sprintf("IPC accept failed: %v", err))
340+
n.log.Error("IPC accept failed", "err", err)
341341
continue
342342
}
343343
go handler.ServeCodec(rpc.NewJSONCodec(conn), rpc.OptionMethodInvocation|rpc.OptionSubscriptions)
@@ -356,7 +356,7 @@ func (n *Node) stopIPC() {
356356
n.ipcListener.Close()
357357
n.ipcListener = nil
358358

359-
n.log.Info(fmt.Sprintf("IPC endpoint closed: %s", n.ipcEndpoint))
359+
n.log.Info("IPC endpoint closed", "endpoint", n.ipcEndpoint)
360360
}
361361
if n.ipcHandler != nil {
362362
n.ipcHandler.Stop()
@@ -365,7 +365,7 @@ func (n *Node) stopIPC() {
365365
}
366366

367367
// startHTTP initializes and starts the HTTP RPC endpoint.
368-
func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors []string) error {
368+
func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors []string, vhosts []string) error {
369369
// Short circuit if the HTTP endpoint isn't being exposed
370370
if endpoint == "" {
371371
return nil
@@ -382,7 +382,7 @@ func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors
382382
if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
383383
return err
384384
}
385-
n.log.Debug(fmt.Sprintf("HTTP registered %T under '%s'", api.Service, api.Namespace))
385+
n.log.Debug("HTTP registered", "service", api.Service, "namespace", api.Namespace)
386386
}
387387
}
388388
// All APIs registered, start the HTTP listener
@@ -393,9 +393,8 @@ func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors
393393
if listener, err = net.Listen("tcp", endpoint); err != nil {
394394
return err
395395
}
396-
go rpc.NewHTTPServer(cors, handler).Serve(listener)
397-
n.log.Info(fmt.Sprintf("HTTP endpoint opened: http://%s", endpoint))
398-
396+
go rpc.NewHTTPServer(cors, vhosts, handler).Serve(listener)
397+
n.log.Info("HTTP endpoint opened", "url", fmt.Sprintf("http://%s", endpoint), "cors", strings.Join(cors, ","), "hvosts", strings.Join(vhosts, ","))
399398
// All listeners booted successfully
400399
n.httpEndpoint = endpoint
401400
n.httpListener = listener
@@ -410,7 +409,7 @@ func (n *Node) stopHTTP() {
410409
n.httpListener.Close()
411410
n.httpListener = nil
412411

413-
n.log.Info(fmt.Sprintf("HTTP endpoint closed: http://%s", n.httpEndpoint))
412+
n.log.Info("HTTP endpoint closed", "url", fmt.Sprintf("http://%s", n.httpEndpoint))
414413
}
415414
if n.httpHandler != nil {
416415
n.httpHandler.Stop()
@@ -436,7 +435,7 @@ func (n *Node) startWS(endpoint string, apis []rpc.API, modules []string, wsOrig
436435
if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
437436
return err
438437
}
439-
n.log.Debug(fmt.Sprintf("WebSocket registered %T under '%s'", api.Service, api.Namespace))
438+
n.log.Debug("WebSocket registered", "service", api.Service, "namespace", api.Namespace)
440439
}
441440
}
442441
// All APIs registered, start the HTTP listener
@@ -448,7 +447,7 @@ func (n *Node) startWS(endpoint string, apis []rpc.API, modules []string, wsOrig
448447
return err
449448
}
450449
go rpc.NewWSServer(wsOrigins, handler).Serve(listener)
451-
n.log.Info(fmt.Sprintf("WebSocket endpoint opened: ws://%s", listener.Addr()))
450+
n.log.Info("WebSocket endpoint opened", "url", fmt.Sprintf("ws://%s", listener.Addr()))
452451

453452
// All listeners booted successfully
454453
n.wsEndpoint = endpoint
@@ -464,7 +463,7 @@ func (n *Node) stopWS() {
464463
n.wsListener.Close()
465464
n.wsListener = nil
466465

467-
n.log.Info(fmt.Sprintf("WebSocket endpoint closed: ws://%s", n.wsEndpoint))
466+
n.log.Info("WebSocket endpoint closed", "url", fmt.Sprintf("ws://%s", n.wsEndpoint))
468467
}
469468
if n.wsHandler != nil {
470469
n.wsHandler.Stop()

p2p/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ type Config struct {
142142
EnableMsgEvents bool
143143

144144
// Logger is a custom logger to use with the p2p.Server.
145-
Logger log.Logger
145+
Logger log.Logger `toml:",omitempty"`
146146
}
147147

148148
// Server manages all peer connections.

rpc/http.go

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"time"
3232

3333
"github.com/rs/cors"
34+
"strings"
3435
)
3536

3637
const (
@@ -148,8 +149,11 @@ func (t *httpReadWriteNopCloser) Close() error {
148149
// NewHTTPServer creates a new HTTP RPC server around an API provider.
149150
//
150151
// Deprecated: Server implements http.Handler
151-
func NewHTTPServer(cors []string, srv *Server) *http.Server {
152-
return &http.Server{Handler: newCorsHandler(srv, cors)}
152+
func NewHTTPServer(cors []string, vhosts []string, srv *Server) *http.Server {
153+
// Wrap the CORS-handler within a host-handler
154+
handler := newCorsHandler(srv, cors)
155+
handler = newVHostHandler(vhosts, handler)
156+
return &http.Server{Handler: handler}
153157
}
154158

155159
// ServeHTTP serves JSON-RPC requests over HTTP.
@@ -195,7 +199,6 @@ func newCorsHandler(srv *Server, allowedOrigins []string) http.Handler {
195199
if len(allowedOrigins) == 0 {
196200
return srv
197201
}
198-
199202
c := cors.New(cors.Options{
200203
AllowedOrigins: allowedOrigins,
201204
AllowedMethods: []string{http.MethodPost, http.MethodGet},
@@ -204,3 +207,51 @@ func newCorsHandler(srv *Server, allowedOrigins []string) http.Handler {
204207
})
205208
return c.Handler(srv)
206209
}
210+
211+
// virtualHostHandler is a handler which validates the Host-header of incoming requests.
212+
// The virtualHostHandler can prevent DNS rebinding attacks, which do not utilize CORS-headers,
213+
// since they do in-domain requests against the RPC api. Instead, we can see on the Host-header
214+
// which domain was used, and validate that against a whitelist.
215+
type virtualHostHandler struct {
216+
vhosts map[string]struct{}
217+
next http.Handler
218+
}
219+
220+
// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler
221+
func (h *virtualHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
222+
// if r.Host is not set, we can continue serving since a browser would set the Host header
223+
if r.Host == "" {
224+
h.next.ServeHTTP(w, r)
225+
return
226+
}
227+
host, _, err := net.SplitHostPort(r.Host)
228+
if err != nil {
229+
// Either invalid (too many colons) or no port specified
230+
host = r.Host
231+
}
232+
if ipAddr := net.ParseIP(host); ipAddr != nil {
233+
// It's an IP address, we can serve that
234+
h.next.ServeHTTP(w, r)
235+
return
236+
237+
}
238+
// Not an ip address, but a hostname. Need to validate
239+
if _, exist := h.vhosts["*"]; exist {
240+
h.next.ServeHTTP(w, r)
241+
return
242+
}
243+
if _, exist := h.vhosts[host]; exist {
244+
h.next.ServeHTTP(w, r)
245+
return
246+
}
247+
http.Error(w, "invalid host specified", http.StatusForbidden)
248+
return
249+
}
250+
251+
func newVHostHandler(vhosts []string, next http.Handler) http.Handler {
252+
vhostMap := make(map[string]struct{})
253+
for _, allowedHost := range vhosts {
254+
vhostMap[strings.ToLower(allowedHost)] = struct{}{}
255+
}
256+
return &virtualHostHandler{vhostMap, next}
257+
}

0 commit comments

Comments
 (0)