Skip to content

Commit 1aa143d

Browse files
committed
fix(console,internal/ethapi,node,rpc): restrict debug_setHead to local transports
Add a local-only RPC API classification and use it to keep debug_setHead available over in-process RPC and IPC while hiding it from HTTP and WebSocket transports. This also updates the admin RPC startup path and adds regression coverage for console visibility, transport exposure, and local-only API leakage.
1 parent 146252a commit 1aa143d

13 files changed

Lines changed: 630 additions & 59 deletions

File tree

cmd/XDC/consolecmd.go

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

1919
import (
2020
"fmt"
21+
"net/url"
2122
"os"
2223
"os/signal"
2324
"path/filepath"
@@ -85,10 +86,11 @@ func localConsole(ctx *cli.Context) error {
8586
utils.Fatalf("Failed to attach to the inproc XDC: %v", err)
8687
}
8788
config := console.Config{
88-
DataDir: utils.MakeDataDir(ctx),
89-
DocRoot: ctx.String(utils.JSpathFlag.Name),
90-
Client: client,
91-
Preload: utils.MakeConsolePreloads(ctx),
89+
DataDir: utils.MakeDataDir(ctx),
90+
DocRoot: ctx.String(utils.JSpathFlag.Name),
91+
Client: client,
92+
LocalTransport: true,
93+
Preload: utils.MakeConsolePreloads(ctx),
9294
}
9395

9496
console, err := console.New(config)
@@ -132,15 +134,16 @@ func remoteConsole(ctx *cli.Context) error {
132134
endpoint = fmt.Sprintf("%s/XDC.ipc", path)
133135
}
134136

135-
client, err := dialRPC(endpoint)
137+
client, localTransport, err := dialRPC(endpoint)
136138
if err != nil {
137139
utils.Fatalf("Unable to attach to remote XDC: %v", err)
138140
}
139141
config := console.Config{
140-
DataDir: utils.MakeDataDir(ctx),
141-
DocRoot: ctx.String(utils.JSpathFlag.Name),
142-
Client: client,
143-
Preload: utils.MakeConsolePreloads(ctx),
142+
DataDir: utils.MakeDataDir(ctx),
143+
DocRoot: ctx.String(utils.JSpathFlag.Name),
144+
Client: client,
145+
LocalTransport: localTransport,
146+
Preload: utils.MakeConsolePreloads(ctx),
144147
}
145148

146149
console, err := console.New(config)
@@ -164,15 +167,39 @@ func remoteConsole(ctx *cli.Context) error {
164167
// dialRPC returns a RPC client which connects to the given endpoint.
165168
// The check for empty endpoint implements the defaulting logic
166169
// for "XDC attach" and "XDC monitor" with no argument.
167-
func dialRPC(endpoint string) (*rpc.Client, error) {
170+
func dialRPC(endpoint string) (*rpc.Client, bool, error) {
171+
endpoint, localTransport := resolveConsoleEndpoint(endpoint)
172+
client, err := rpc.Dial(endpoint)
173+
return client, localTransport, err
174+
}
175+
176+
func resolveConsoleEndpoint(endpoint string) (string, bool) {
168177
if endpoint == "" {
169-
endpoint = node.DefaultIPCEndpoint(clientIdentifier)
170-
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
171-
// Backwards compatibility with geth < 1.5 which required
172-
// these prefixes.
173-
endpoint = endpoint[4:]
178+
return node.DefaultIPCEndpoint(clientIdentifier), true
179+
}
180+
if strings.HasPrefix(endpoint, "ipc:") {
181+
// Backwards compatibility with geth < 1.5 which required these prefixes.
182+
return endpoint[4:], true
183+
}
184+
// Backwards compatibility with geth < 1.5 which required this prefix.
185+
// Strip the legacy prefix, then classify the resulting endpoint based
186+
// on its actual transport instead of assuming it is local.
187+
endpoint = strings.TrimPrefix(endpoint, "rpc:")
188+
if endpoint == "stdio" {
189+
return endpoint, false
190+
}
191+
u, err := url.Parse(endpoint)
192+
if err != nil {
193+
return endpoint, false
194+
}
195+
switch u.Scheme {
196+
case "http", "https", "ws", "wss", "stdio":
197+
return endpoint, false
198+
case "":
199+
return endpoint, true
200+
default:
201+
return endpoint, false
174202
}
175-
return rpc.Dial(endpoint)
176203
}
177204

178205
// ephemeralConsole starts a new XDC node, attaches an ephemeral JavaScript
@@ -190,10 +217,11 @@ func ephemeralConsole(ctx *cli.Context) error {
190217
utils.Fatalf("Failed to attach to the inproc XDC: %v", err)
191218
}
192219
config := console.Config{
193-
DataDir: utils.MakeDataDir(ctx),
194-
DocRoot: ctx.String(utils.JSpathFlag.Name),
195-
Client: client,
196-
Preload: utils.MakeConsolePreloads(ctx),
220+
DataDir: utils.MakeDataDir(ctx),
221+
DocRoot: ctx.String(utils.JSpathFlag.Name),
222+
Client: client,
223+
LocalTransport: true,
224+
Preload: utils.MakeConsolePreloads(ctx),
197225
}
198226

199227
console, err := console.New(config)

cmd/XDC/consolecmd_test.go

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package main
1919
import (
2020
"crypto/rand"
2121
"math/big"
22+
"net"
2223
"path/filepath"
2324
"runtime"
2425
"strconv"
@@ -96,7 +97,7 @@ func TestIPCAttachWelcome(t *testing.T) {
9697

9798
func TestHTTPAttachWelcome(t *testing.T) {
9899
coinbase := "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
99-
port := strconv.Itoa(trulyRandInt(1024, 65536)) // Yeah, sometimes this will fail, sorry :P
100+
port := strconv.Itoa(freeTCPPort(t))
100101
datadir := t.TempDir()
101102
XDC := runXDC(t,
102103
"--datadir", datadir, "--XDCx-datadir", datadir+"/XDCx",
@@ -112,7 +113,7 @@ func TestHTTPAttachWelcome(t *testing.T) {
112113

113114
func TestWSAttachWelcome(t *testing.T) {
114115
coinbase := "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
115-
port := strconv.Itoa(trulyRandInt(1024, 65536)) // Yeah, sometimes this will fail, sorry :P
116+
port := strconv.Itoa(freeTCPPort(t))
116117
datadir := t.TempDir()
117118
XDC := runXDC(t,
118119
"--datadir", datadir, "--XDCx-datadir", datadir+"/XDCx",
@@ -160,6 +161,89 @@ at block: 0 ({{niltime}}){{if ipc}}
160161
attach.ExpectExit()
161162
}
162163

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

cmd/XDC/run_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ func TestMain(m *testing.M) {
5757
func runXDC(t *testing.T, args ...string) *testXDC {
5858
tt := &testXDC{}
5959
tt.TestCmd = cmdtest.NewTestCmd(t, tt)
60+
var extraArgs []string
61+
if !hasArg(args, "--http-port") {
62+
extraArgs = append(extraArgs, "--http-port", "0")
63+
}
64+
if !hasArg(args, "--ws-port") {
65+
extraArgs = append(extraArgs, "--ws-port", "0")
66+
}
67+
if len(extraArgs) > 0 {
68+
args = append(extraArgs, args...)
69+
}
6070
for i, arg := range args {
6171
switch arg {
6272
case "--datadir":
@@ -82,3 +92,12 @@ func runXDC(t *testing.T, args ...string) *testXDC {
8292

8393
return tt
8494
}
95+
96+
func hasArg(args []string, want string) bool {
97+
for _, arg := range args {
98+
if arg == want {
99+
return true
100+
}
101+
}
102+
return false
103+
}

console/console.go

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

6364
// Console is a JavaScript interpreted runtime environment. It is a fully fleged
6465
// JavaScript console attached to a running node via an external or in-process RPC
6566
// client.
6667
type Console struct {
67-
client *rpc.Client // RPC client to execute Ethereum requests through
68-
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
69-
prompt string // Input prompt prefix string
70-
prompter UserPrompter // Input prompter to allow interactive user feedback
71-
histPath string // Absolute path to the console scrollback history
72-
history []string // Scroll history maintained by the console
73-
printer io.Writer // Output writer to serialize any display strings to
68+
client *rpc.Client // RPC client to execute Ethereum requests through
69+
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
70+
localTransport bool // Whether the connected transport is in-process or IPC
71+
prompt string // Input prompt prefix string
72+
prompter UserPrompter // Input prompter to allow interactive user feedback
73+
histPath string // Absolute path to the console scrollback history
74+
history []string // Scroll history maintained by the console
75+
printer io.Writer // Output writer to serialize any display strings to
7476
}
7577

7678
// New initializes a JavaScript interpreted runtime environment and sets defaults
@@ -89,12 +91,13 @@ func New(config Config) (*Console, error) {
8991

9092
// Initialize the console and return
9193
console := &Console{
92-
client: config.Client,
93-
jsre: jsre.New(config.DocRoot, config.Printer),
94-
prompt: config.Prompt,
95-
prompter: config.Prompter,
96-
printer: config.Printer,
97-
histPath: filepath.Join(config.DataDir, HistoryFile),
94+
client: config.Client,
95+
jsre: jsre.New(config.DocRoot, config.Printer),
96+
localTransport: config.LocalTransport,
97+
prompt: config.Prompt,
98+
prompter: config.Prompter,
99+
printer: config.Printer,
100+
histPath: filepath.Join(config.DataDir, HistoryFile),
98101
}
99102
if err := os.MkdirAll(config.DataDir, 0700); err != nil {
100103
return nil, err
@@ -207,9 +210,41 @@ func (c *Console) initExtensions() error {
207210
}
208211
}
209212
})
213+
if !c.localTransport {
214+
c.hideUnavailableDebugMethods()
215+
}
210216
return nil
211217
}
212218

219+
func (c *Console) hideUnavailableDebugMethods() {
220+
c.jsre.Do(func(vm *goja.Runtime) {
221+
if _, err := vm.RunString(`
222+
(function() {
223+
function hideMethod(obj, hidden) {
224+
if (obj == null) {
225+
return;
226+
}
227+
Object.defineProperty(obj, hidden, {
228+
value: undefined,
229+
writable: true,
230+
configurable: true,
231+
enumerable: false
232+
});
233+
}
234+
235+
if (typeof debug !== "undefined") {
236+
hideMethod(debug, "setHead");
237+
}
238+
if (typeof web3 !== "undefined" && web3 !== null) {
239+
hideMethod(web3.debug, "setHead");
240+
}
241+
})();
242+
`); err != nil {
243+
panic(err)
244+
}
245+
})
246+
}
247+
213248
// initAdmin creates additional admin APIs implemented by the bridge.
214249
func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
215250
if admin := getObject(vm, "admin"); admin != nil {
@@ -260,7 +295,21 @@ func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, str
260295
start++
261296
break
262297
}
263-
return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
298+
return line[:start], c.filterCompletions(c.jsre.CompleteKeywords(line[start:pos])), line[pos:]
299+
}
300+
301+
func (c *Console) filterCompletions(completions []string) []string {
302+
if c.localTransport {
303+
return completions
304+
}
305+
filtered := completions[:0]
306+
for _, completion := range completions {
307+
if completion == "debug.setHead" || completion == "debug.setHead(" || completion == "debug.setHead." || completion == "web3.debug.setHead" || completion == "web3.debug.setHead(" || completion == "web3.debug.setHead." {
308+
continue
309+
}
310+
filtered = append(filtered, completion)
311+
}
312+
return filtered
264313
}
265314

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

0 commit comments

Comments
 (0)