Skip to content

Commit 9f1d5de

Browse files
committed
fix(internal/ethapi,console,node,rpc): restrict debug_setHead to local transports
Move debug_setHead out of the public debug API and expose it only through local transports by introducing a local-only RPC API classification. This keeps debug_setHead available to in-process clients, IPC, and the local console while removing it from HTTP and WebSocket JSON-RPC exposure. It also updates the console and node tests to cover the new visibility rules.
1 parent f5fe86c commit 9f1d5de

13 files changed

Lines changed: 593 additions & 58 deletions

File tree

cmd/XDC/consolecmd.go

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package main
1818

1919
import (
2020
"fmt"
21+
"net/url"
2122
"slices"
2223
"strings"
2324

@@ -78,10 +79,11 @@ func localConsole(ctx *cli.Context) error {
7879
// Attach to the newly started node and create the JavaScript console.
7980
client := stack.Attach()
8081
config := console.Config{
81-
DataDir: utils.MakeDataDir(ctx),
82-
DocRoot: ctx.String(utils.JSpathFlag.Name),
83-
Client: client,
84-
Preload: utils.MakeConsolePreloads(ctx),
82+
DataDir: utils.MakeDataDir(ctx),
83+
DocRoot: ctx.String(utils.JSpathFlag.Name),
84+
Client: client,
85+
LocalTransport: true,
86+
Preload: utils.MakeConsolePreloads(ctx),
8587
}
8688
console, err := console.New(config)
8789
if err != nil {
@@ -134,15 +136,16 @@ func remoteConsole(ctx *cli.Context) error {
134136
endpoint = cfg.IPCEndpoint()
135137
}
136138

137-
client, err := dialRPC(endpoint)
139+
client, localTransport, err := dialRPC(endpoint)
138140
if err != nil {
139141
utils.Fatalf("Unable to attach to remote XDC: %v", err)
140142
}
141143
config := console.Config{
142-
DataDir: utils.MakeDataDir(ctx),
143-
DocRoot: ctx.String(utils.JSpathFlag.Name),
144-
Client: client,
145-
Preload: utils.MakeConsolePreloads(ctx),
144+
DataDir: utils.MakeDataDir(ctx),
145+
DocRoot: ctx.String(utils.JSpathFlag.Name),
146+
Client: client,
147+
LocalTransport: localTransport,
148+
Preload: utils.MakeConsolePreloads(ctx),
146149
}
147150
console, err := console.New(config)
148151
if err != nil {
@@ -177,13 +180,33 @@ XDC --exec "%s" console`, b.String())
177180
// dialRPC returns a RPC client which connects to the given endpoint.
178181
// The check for empty endpoint implements the defaulting logic
179182
// for "XDC attach" and "XDC monitor" with no argument.
180-
func dialRPC(endpoint string) (*rpc.Client, error) {
183+
func dialRPC(endpoint string) (*rpc.Client, bool, error) {
184+
endpoint, localTransport := resolveConsoleEndpoint(endpoint)
185+
client, err := rpc.Dial(endpoint)
186+
return client, localTransport, err
187+
}
188+
189+
func resolveConsoleEndpoint(endpoint string) (string, bool) {
181190
if endpoint == "" {
182-
endpoint = node.DefaultIPCEndpoint(clientIdentifier)
183-
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
184-
// Backwards compatibility with geth < 1.5 which required
185-
// these prefixes.
186-
endpoint = endpoint[4:]
191+
return node.DefaultIPCEndpoint(clientIdentifier), true
192+
}
193+
if strings.HasPrefix(endpoint, "ipc:") {
194+
return endpoint[4:], true
195+
}
196+
endpoint = strings.TrimPrefix(endpoint, "rpc:")
197+
if endpoint == "stdio" {
198+
return endpoint, false
199+
}
200+
u, err := url.Parse(endpoint)
201+
if err != nil {
202+
return endpoint, false
203+
}
204+
switch u.Scheme {
205+
case "http", "https", "ws", "wss", "stdio":
206+
return endpoint, false
207+
case "":
208+
return endpoint, true
209+
default:
210+
return endpoint, false
187211
}
188-
return rpc.Dial(endpoint)
189212
}

cmd/XDC/consolecmd_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,77 @@ To exit, press ctrl-d or type exit
168168
attach.ExpectExit()
169169
}
170170

171+
func TestResolveConsoleEndpoint(t *testing.T) {
172+
tests := []struct {
173+
name string
174+
endpoint string
175+
wantEndpoint string
176+
wantLocal bool
177+
}{
178+
{name: "default ipc endpoint", endpoint: "", wantEndpoint: "", wantLocal: true},
179+
{name: "plain ipc path", endpoint: "/tmp/XDC.ipc", wantEndpoint: "/tmp/XDC.ipc", wantLocal: true},
180+
{name: "legacy ipc prefix", endpoint: "ipc:/tmp/XDC.ipc", wantEndpoint: "/tmp/XDC.ipc", wantLocal: true},
181+
{name: "legacy rpc prefix", endpoint: "rpc:/tmp/XDC.ipc", wantEndpoint: "/tmp/XDC.ipc", wantLocal: true},
182+
{name: "windows drive path stays unsupported", endpoint: `C:\\Users\\tester\\XDC.ipc`, wantEndpoint: `C:\\Users\\tester\\XDC.ipc`, wantLocal: false},
183+
{name: "windows drive slash path stays unsupported", endpoint: "C:/Users/tester/XDC.ipc", wantEndpoint: "C:/Users/tester/XDC.ipc", wantLocal: false},
184+
{name: "legacy rpc windows drive path stays unsupported", endpoint: `rpc:C:\\Users\\tester\\XDC.ipc`, wantEndpoint: `C:\\Users\\tester\\XDC.ipc`, wantLocal: false},
185+
{name: "legacy rpc http endpoint", endpoint: "rpc:http://localhost:8545", wantEndpoint: "http://localhost:8545", wantLocal: false},
186+
{name: "legacy rpc ws endpoint", endpoint: "rpc:ws://localhost:8546", wantEndpoint: "ws://localhost:8546", wantLocal: false},
187+
{name: "stdio endpoint", endpoint: "stdio", wantEndpoint: "stdio", wantLocal: false},
188+
{name: "legacy rpc stdio endpoint", endpoint: "rpc:stdio", wantEndpoint: "stdio", wantLocal: false},
189+
{name: "http endpoint", endpoint: "http://localhost:8545", wantEndpoint: "http://localhost:8545", wantLocal: false},
190+
{name: "ws endpoint", endpoint: "ws://localhost:8546", wantEndpoint: "ws://localhost:8546", wantLocal: false},
191+
}
192+
193+
for _, test := range tests {
194+
t.Run(test.name, func(t *testing.T) {
195+
gotEndpoint, gotLocal := resolveConsoleEndpoint(test.endpoint)
196+
if gotLocal != test.wantLocal {
197+
t.Fatalf("unexpected local transport classification: got %v want %v", gotLocal, test.wantLocal)
198+
}
199+
if test.wantEndpoint == "" {
200+
if !strings.HasSuffix(gotEndpoint, "XDC.ipc") {
201+
t.Fatalf("expected default IPC endpoint, got %q", gotEndpoint)
202+
}
203+
return
204+
}
205+
if gotEndpoint != test.wantEndpoint {
206+
t.Fatalf("unexpected resolved endpoint: got %q want %q", gotEndpoint, test.wantEndpoint)
207+
}
208+
})
209+
}
210+
}
211+
212+
func TestDialRPCRejectsWindowsDrivePaths(t *testing.T) {
213+
tests := []struct {
214+
name string
215+
endpoint string
216+
}{
217+
{name: "windows drive path", endpoint: `C:\\Users\\tester\\XDC.ipc`},
218+
{name: "windows drive slash path", endpoint: "C:/Users/tester/XDC.ipc"},
219+
{name: "legacy rpc windows drive path", endpoint: `rpc:C:\\Users\\tester\\XDC.ipc`},
220+
}
221+
222+
for _, test := range tests {
223+
t.Run(test.name, func(t *testing.T) {
224+
client, local, err := dialRPC(test.endpoint)
225+
if client != nil {
226+
client.Close()
227+
t.Fatal("expected dialRPC to reject Windows drive-letter path")
228+
}
229+
if err == nil {
230+
t.Fatal("expected dialRPC to fail for Windows drive-letter path")
231+
}
232+
if local {
233+
t.Fatal("expected Windows drive-letter path to stay classified as non-local")
234+
}
235+
if !strings.Contains(err.Error(), `no known transport for URL scheme "c"`) {
236+
t.Fatalf("unexpected dialRPC error: %v", err)
237+
}
238+
})
239+
}
240+
}
241+
171242
// trulyRandInt generates a crypto random integer used by the console tests to
172243
// not clash network ports with other tests running cocurrently.
173244
func trulyRandInt(lo, hi int) int {

console/console.go

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,26 +56,28 @@ const DefaultPrompt = "> "
5656
// Config is the collection of configurations to fine tune the behavior of the
5757
// JavaScript console.
5858
type Config struct {
59-
DataDir string // Data directory to store the console history at
60-
DocRoot string // Filesystem path from where to load JavaScript files from
61-
Client *rpc.Client // RPC client to execute Ethereum requests through
62-
Prompt string // Input prompt prefix string (defaults to DefaultPrompt)
63-
Prompter prompt.UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
64-
Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout)
65-
Preload []string // Absolute paths to JavaScript files to preload
59+
DataDir string // Data directory to store the console history at
60+
DocRoot string // Filesystem path from where to load JavaScript files from
61+
Client *rpc.Client // RPC client to execute Ethereum requests through
62+
LocalTransport bool // Whether the console is attached over an in-process or IPC transport
63+
Prompt string // Input prompt prefix string (defaults to DefaultPrompt)
64+
Prompter prompt.UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
65+
Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout)
66+
Preload []string // Absolute paths to JavaScript files to preload
6667
}
6768

6869
// Console is a JavaScript interpreted runtime environment. It is a fully fleged
6970
// JavaScript console attached to a running node via an external or in-process RPC
7071
// client.
7172
type Console struct {
72-
client *rpc.Client // RPC client to execute Ethereum requests through
73-
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
74-
prompt string // Input prompt prefix string
75-
prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
76-
histPath string // Absolute path to the console scrollback history
77-
history []string // Scroll history maintained by the console
78-
printer io.Writer // Output writer to serialize any display strings to
73+
client *rpc.Client // RPC client to execute Ethereum requests through
74+
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
75+
localTransport bool // Whether the connected transport is in-process or IPC
76+
prompt string // Input prompt prefix string
77+
prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
78+
histPath string // Absolute path to the console scrollback history
79+
history []string // Scroll history maintained by the console
80+
printer io.Writer // Output writer to serialize any display strings to
7981

8082
interactiveStopped chan struct{}
8183
stopInteractiveCh chan struct{}
@@ -103,6 +105,7 @@ func New(config Config) (*Console, error) {
103105
console := &Console{
104106
client: config.Client,
105107
jsre: jsre.New(config.DocRoot, config.Printer),
108+
localTransport: config.LocalTransport,
106109
prompt: config.Prompt,
107110
prompter: config.Prompter,
108111
printer: config.Printer,
@@ -235,9 +238,41 @@ func (c *Console) initExtensions() error {
235238
}
236239
}
237240
})
241+
if !c.localTransport {
242+
c.hideUnavailableDebugMethods()
243+
}
238244
return nil
239245
}
240246

247+
func (c *Console) hideUnavailableDebugMethods() {
248+
c.jsre.Do(func(vm *goja.Runtime) {
249+
if _, err := vm.RunString(`
250+
(function() {
251+
function hideMethod(target, name) {
252+
if (!target) {
253+
return;
254+
}
255+
Object.defineProperty(target, name, {
256+
value: undefined,
257+
writable: true,
258+
configurable: true,
259+
enumerable: false
260+
});
261+
}
262+
263+
if (typeof debug !== "undefined") {
264+
hideMethod(debug, "setHead");
265+
}
266+
if (typeof web3 !== "undefined" && web3 && web3.debug) {
267+
hideMethod(web3.debug, "setHead");
268+
}
269+
})();
270+
`); err != nil {
271+
panic(err)
272+
}
273+
})
274+
}
275+
241276
// initAdmin creates additional admin APIs implemented by the bridge.
242277
func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
243278
if admin := getObject(vm, "admin"); admin != nil {
@@ -288,7 +323,23 @@ func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, str
288323
start++
289324
break
290325
}
291-
return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
326+
return line[:start], c.filterCompletions(c.jsre.CompleteKeywords(line[start:pos])), line[pos:]
327+
}
328+
329+
func (c *Console) filterCompletions(completions []string) []string {
330+
if c.localTransport {
331+
return completions
332+
}
333+
filtered := completions[:0]
334+
for _, completion := range completions {
335+
switch completion {
336+
case "debug.setHead", "debug.setHead(", "debug.setHead.", "web3.debug.setHead", "web3.debug.setHead(", "web3.debug.setHead.":
337+
continue
338+
default:
339+
filtered = append(filtered, completion)
340+
}
341+
}
342+
return filtered
292343
}
293344

294345
// Welcome show summary of current Geth instance and some metadata about the

0 commit comments

Comments
 (0)