Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions op-wheel/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func main() {
app.ErrWriter = os.Stderr
app.Commands = []*cli.Command{
wheel.CheatCmd,
wheel.CheatRethCmd,
wheel.EngineCmd,
}

Expand Down
114 changes: 114 additions & 0 deletions op-wheel/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,30 @@ var (
}),
}

EngineRewindRethCmd = &cli.Command{
Name: "rewind-reth",
Description: "Rewind a reth node offline by running 'reth stage unwind'. The reth node must be stopped.",
Flags: rethFlags(
&cli.Uint64Flag{
Name: "to",
Usage: "Block number to rewind chain to",
Required: true,
EnvVars: prefixEnvVars("REWIND_TO"),
},
),
Action: func(ctx *cli.Context) error {
lgr := initLogger(ctx)
return engine.RethRewind(
ctx.Context,
lgr,
ctx.String("reth-binary"),
ctx.String("reth-datadir"),
ctx.String("reth-chain"),
ctx.Uint64("to"),
)
},
}

EngineJSONCmd = &cli.Command{
Name: "json",
Description: "read json values from remaining args, or STDIN, and use them as RPC params to call the engine RPC method (first arg)",
Expand Down Expand Up @@ -700,6 +724,95 @@ var (
}
)

func rethFlags(flags ...cli.Flag) []cli.Flag {
return append(append(flags,
&cli.StringFlag{
Name: "reth-binary",
Usage: "Path to the reth binary",
Required: true,
EnvVars: prefixEnvVars("RETH_BINARY"),
},
&cli.StringFlag{
Name: "reth-datadir",
Usage: "Reth data directory path",
Required: true,
EnvVars: prefixEnvVars("RETH_DATADIR"),
},
&cli.StringFlag{
Name: "reth-chain",
Usage: "Chain spec name or path (e.g., 'optimism', 'dev', or path to genesis file)",
Required: true,
EnvVars: prefixEnvVars("RETH_CHAIN"),
},
), oplog.CLIFlags(envVarPrefix)...)
}

var (
CheatRethStateCmd = &cli.Command{
Name: "state",
Description: "Read account state (balance, nonce, code, storage) from a reth database offline.",
Flags: rethFlags(
&cli.StringFlag{
Name: "address",
Usage: "Account address to inspect",
Required: true,
EnvVars: prefixEnvVars("ADDRESS"),
},
&cli.StringFlag{
Name: "block",
Usage: "Block number to query state at (uses latest if not provided)",
EnvVars: prefixEnvVars("BLOCK"),
},
&cli.Uint64Flag{
Name: "limit",
Usage: "Maximum number of storage slots to display",
Value: 100,
EnvVars: prefixEnvVars("LIMIT"),
},
),
Action: func(ctx *cli.Context) error {
lgr := initLogger(ctx)
return engine.RethState(
ctx.Context,
lgr,
ctx.String("reth-binary"),
ctx.String("reth-datadir"),
ctx.String("reth-chain"),
ctx.String("address"),
ctx.String("block"),
ctx.Uint64("limit"),
)
},
}

CheatRethHeadCmd = &cli.Command{
Name: "head",
Description: "Show the current head block via stage checkpoints from a reth database offline.",
Flags: rethFlags(),
Action: func(ctx *cli.Context) error {
lgr := initLogger(ctx)
return engine.RethHead(
ctx.Context,
lgr,
ctx.String("reth-binary"),
ctx.String("reth-datadir"),
ctx.String("reth-chain"),
)
},
}
)

var CheatRethCmd = &cli.Command{
Name: "cheat-reth",
Usage: "Read-only inspection commands for a reth database (offline).",
Description: "Each sub-command invokes reth CLI tools against the database. " +
"The reth node must be stopped before running these commands.",
Subcommands: []*cli.Command{
CheatRethStateCmd,
CheatRethHeadCmd,
},
}

var CheatCmd = &cli.Command{
Name: "cheat",
Usage: "Cheating commands to modify a Geth database.",
Expand Down Expand Up @@ -728,6 +841,7 @@ var EngineCmd = &cli.Command{
EngineSetForkchoiceCmd,
EngineSetForkchoiceHashCmd,
EngineRewindCmd,
EngineRewindRethCmd,
EngineJSONCmd,
},
}
103 changes: 103 additions & 0 deletions op-wheel/engine/reth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package engine

import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strconv"

"github.com/ethereum/go-ethereum/log"
)

// RethRewind performs an offline rewind of a reth node by executing
// `reth stage unwind to-block <N>` as a subprocess.
// The reth node must be stopped before calling this function.
func RethRewind(ctx context.Context, lgr log.Logger, rethBinary string, datadir string, chain string, toBlock uint64) error {
cmd, err := buildRethUnwindCmd(ctx, rethBinary, datadir, chain, toBlock)
if err != nil {
return err
}
return runRethCmd(ctx, lgr, cmd, "reth stage unwind")
}

// RethState runs `reth db state <address>` to inspect account state offline.
func RethState(ctx context.Context, lgr log.Logger, rethBinary string, datadir string, chain string, address string, block string, limit uint64) error {
cmd, err := buildRethDBCmd(ctx, rethBinary, datadir, chain, "state", address, "--format", "json", "--limit", strconv.FormatUint(limit, 10))
if err != nil {
return err
}
if block != "" {
cmd.Args = append(cmd.Args, "--block", block)
}
return runRethCmd(ctx, lgr, cmd, "reth db state")
}

// RethHead runs `reth db stage-checkpoints get` to show the current head (stage checkpoints).
func RethHead(ctx context.Context, lgr log.Logger, rethBinary string, datadir string, chain string) error {
cmd, err := buildRethDBCmd(ctx, rethBinary, datadir, chain, "stage-checkpoints", "get")
if err != nil {
return err
}
return runRethCmd(ctx, lgr, cmd, "reth db stage-checkpoints")
}

// runRethCmd executes a reth command, streaming output and handling exit codes.
func runRethCmd(_ context.Context, lgr log.Logger, cmd *exec.Cmd, label string) error {
lgr.Info("Executing "+label, "binary", cmd.Path, "args", cmd.Args[1:])

cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return fmt.Errorf("%s failed with exit code %d: %w", label, exitErr.ExitCode(), err)
}
return fmt.Errorf("failed to execute reth: %w", err)
}

lgr.Info(label + " completed successfully")
return nil
}

// resolveRethBinary validates the reth binary exists and returns its resolved path.
func resolveRethBinary(rethBinary string) (string, error) {
resolvedPath, err := exec.LookPath(rethBinary)
if err != nil {
return "", fmt.Errorf("reth binary not found at %q: %w", rethBinary, err)
}
return resolvedPath, nil
}

// buildRethUnwindCmd constructs the exec.Cmd for `reth stage unwind to-block <N>`.
func buildRethUnwindCmd(ctx context.Context, rethBinary string, datadir string, chain string, toBlock uint64) (*exec.Cmd, error) {
resolvedPath, err := resolveRethBinary(rethBinary)
if err != nil {
return nil, err
}

args := []string{
"stage", "unwind",
"--datadir", datadir,
"--chain", chain,
"to-block", strconv.FormatUint(toBlock, 10),
}

return exec.CommandContext(ctx, resolvedPath, args...), nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium SAST Finding

OS Command Injection (CWE-78)

More Details

OS command injection is a critical vulnerability that allows an attacker to execute arbitrary commands on the system. This can lead to a full system compromise, as the attacker can potentially gain complete control over the application and the underlying system.

The vulnerability arises when user input is used to construct commands or command arguments that are then executed by the application. This can occur when user-supplied data is passed directly to functions that execute OS commands, such as exec.Command, exec.CommandContext, syscall.ForkExec, or syscall.StartProcess.

If an attacker can inject malicious code into these commands, they can potentially execute any command on the system, including installing malware, stealing data, or causing a denial of service. This vulnerability can have severe consequences, including data breaches, system compromise, and unauthorized access to sensitive information.

To avoid this vulnerability, user input should never be used directly in constructing commands or command arguments. Instead, the application should use a hardcoded set of arguments and validate any user input to ensure it does not contain malicious code.

Attribute Value
Impact Medium
Likelihood Medium

Remediation

To remediate this vulnerability, follow these recommendations:

  • Never use user-supplied input directly in constructing commands or command arguments.
  • Validate and sanitize all user input before using it in any context.
  • Use a hardcoded set of arguments for executing OS commands.
  • If filenames are required, use a hash or unique identifier instead of the actual filename.
  • Consider using a native library that implements the required functionality instead of executing OS commands.

Example of safely executing an OS command:

userData := []byte("user data")
// Create a temporary file in the application-specific directory
f, err := ioutil.TempFile("/var/app/restricted", "temp-*.dat")
if err != nil {
  log.Fatal(err)
}

if _, err := f.Write(userData); err != nil {
  log.Fatal(err)
}

if err := f.Close(); err != nil {
  log.Fatal(err)
}

// Pass the full path to the binary and the name of the temporary file
out, err := exec.Command("/bin/cat", f.Name()).Output()
if err != nil {
  log.Fatal(err)
}

Rule ID: WS-I011-GO-00026


To ignore this finding as an exception, reply to this conversation with #wiz_ignore reason

If you'd like to ignore this finding in all future scans, add an exception in the .wiz file (learn more) or create an Ignore Rule (learn more).


To get more details on how to remediate this issue using AI, reply to this conversation with #wiz remediate

}

// buildRethDBCmd constructs an exec.Cmd for `reth db --datadir <dir> --chain <chain> <subcommand> [args...]`.
func buildRethDBCmd(ctx context.Context, rethBinary string, datadir string, chain string, subArgs ...string) (*exec.Cmd, error) {
resolvedPath, err := resolveRethBinary(rethBinary)
if err != nil {
return nil, err
}

// reth db --datadir <dir> --chain <chain> <subcommand> [args...]
args := []string{"db", "--datadir", datadir, "--chain", chain}
args = append(args, subArgs...)

return exec.CommandContext(ctx, resolvedPath, args...), nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium SAST Finding

OS Command Injection (CWE-78)

More Details

OS command injection is a critical vulnerability that allows an attacker to execute arbitrary commands on the system. This can lead to a full system compromise, as the attacker can potentially gain complete control over the application and the underlying system.

The vulnerability arises when user input is used to construct commands or command arguments that are then executed by the application. This can occur when user-supplied data is passed directly to functions that execute OS commands, such as exec.Command, exec.CommandContext, syscall.ForkExec, or syscall.StartProcess.

If an attacker can inject malicious code into these commands, they can potentially execute any command on the system, including installing malware, stealing data, or causing a denial of service. This vulnerability can have severe consequences, including data breaches, system compromise, and unauthorized access to sensitive information.

To avoid this vulnerability, user input should never be used directly in constructing commands or command arguments. Instead, the application should use a hardcoded set of arguments and validate any user input to ensure it does not contain malicious code.

Attribute Value
Impact Medium
Likelihood Medium

Remediation

To remediate this vulnerability, follow these recommendations:

  • Never use user-supplied input directly in constructing commands or command arguments.
  • Validate and sanitize all user input before using it in any context.
  • Use a hardcoded set of arguments for executing OS commands.
  • If filenames are required, use a hash or unique identifier instead of the actual filename.
  • Consider using a native library that implements the required functionality instead of executing OS commands.

Example of safely executing an OS command:

userData := []byte("user data")
// Create a temporary file in the application-specific directory
f, err := ioutil.TempFile("/var/app/restricted", "temp-*.dat")
if err != nil {
  log.Fatal(err)
}

if _, err := f.Write(userData); err != nil {
  log.Fatal(err)
}

if err := f.Close(); err != nil {
  log.Fatal(err)
}

// Pass the full path to the binary and the name of the temporary file
out, err := exec.Command("/bin/cat", f.Name()).Output()
if err != nil {
  log.Fatal(err)
}

Rule ID: WS-I011-GO-00026


To ignore this finding as an exception, reply to this conversation with #wiz_ignore reason

If you'd like to ignore this finding in all future scans, add an exception in the .wiz file (learn more) or create an Ignore Rule (learn more).


To get more details on how to remediate this issue using AI, reply to this conversation with #wiz remediate

}
132 changes: 132 additions & 0 deletions op-wheel/engine/reth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package engine

import (
"context"
"fmt"
"os"
"os/exec"
"strconv"
"testing"

"github.com/ethereum/go-ethereum/log"
)

func TestBuildRethUnwindCmd_Args(t *testing.T) {
self, err := os.Executable()
if err != nil {
t.Fatal(err)
}

cmd, err := buildRethUnwindCmd(context.Background(), self, "/data/reth", "op-mainnet", 12345678)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

wantArgs := []string{
self,
"stage", "unwind",
"--datadir", "/data/reth",
"--chain", "op-mainnet",
"to-block", "12345678",
}
assertArgs(t, cmd.Args, wantArgs)
}

func TestBuildRethUnwindCmd_BinaryNotFound(t *testing.T) {
_, err := buildRethUnwindCmd(context.Background(), "/nonexistent/reth", "/data", "optimism", 100)
if err == nil {
t.Fatal("expected error for nonexistent binary, got nil")
}
}

func TestBuildRethDBCmd_State(t *testing.T) {
self, err := os.Executable()
if err != nil {
t.Fatal(err)
}

cmd, err := buildRethDBCmd(context.Background(), self, "/db", "optimism",
"state", "0xdead", "--format", "json", "--limit", "100")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

wantArgs := []string{
self,
"db", "--datadir", "/db", "--chain", "optimism",
"state", "0xdead", "--format", "json", "--limit", "100",
}
assertArgs(t, cmd.Args, wantArgs)
}

func TestBuildRethDBCmd_StageCheckpoints(t *testing.T) {
self, err := os.Executable()
if err != nil {
t.Fatal(err)
}

cmd, err := buildRethDBCmd(context.Background(), self, "/db", "dev",
"stage-checkpoints", "get")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

wantArgs := []string{
self,
"db", "--datadir", "/db", "--chain", "dev",
"stage-checkpoints", "get",
}
assertArgs(t, cmd.Args, wantArgs)
}

func TestRethRewind_SubprocessExit(t *testing.T) {
if os.Getenv("GO_TEST_HELPER_PROCESS") == "1" {
code, _ := strconv.Atoi(os.Getenv("GO_TEST_EXIT_CODE"))
os.Exit(code)
}

self, err := os.Executable()
if err != nil {
t.Fatal(err)
}
lgr := log.NewLogger(log.DiscardHandler())

t.Run("success", func(t *testing.T) {
err := rethCmdWithHelper(context.Background(), lgr, self, 0, "test")
if err != nil {
t.Fatalf("expected success, got: %v", err)
}
})

t.Run("failure", func(t *testing.T) {
err := rethCmdWithHelper(context.Background(), lgr, self, 1, "test")
if err == nil {
t.Fatal("expected error for exit code 1, got nil")
}
})
}

// rethCmdWithHelper runs a subprocess using the test binary as a fake reth,
// configured to exit with the given code.
func rethCmdWithHelper(ctx context.Context, lgr log.Logger, testBinary string, exitCode int, label string) error {
cmd := exec.CommandContext(ctx, testBinary,
"-test.run=TestRethRewind_SubprocessExit",
)
cmd.Env = append(os.Environ(),
"GO_TEST_HELPER_PROCESS=1",
fmt.Sprintf("GO_TEST_EXIT_CODE=%d", exitCode),
)
return runRethCmd(ctx, lgr, cmd, label)
}

func assertArgs(t *testing.T, got, want []string) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("args length mismatch: got %d, want %d\ngot: %v\nwant: %v", len(got), len(want), got, want)
}
for i, w := range want {
if got[i] != w {
t.Errorf("arg[%d] = %q, want %q", i, got[i], w)
}
}
}
Loading
Loading