Skip to content
Merged
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
150 changes: 149 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 46 additions & 14 deletions packages/cli-sdk/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { WalletConnectCLI } from "./client.js";
import { resolveProjectId, setConfigValue, getConfigValue } from "./config.js";

const METADATA = {
name: "walletconnect",
Expand All @@ -11,23 +12,28 @@ function usage(): void {
console.log(`Usage: walletconnect <command> [options]

Commands:
connect Connect to a wallet via QR code
whoami Show current session info
sign <message> Sign a message with the connected wallet
disconnect Disconnect the current session
connect Connect to a wallet via QR code
whoami Show current session info
sign <message> Sign a message with the connected wallet
disconnect Disconnect the current session
config set <k> <v> Set a config value (e.g. project-id)
config get <k> Get a config value

Options:
--browser Use browser UI instead of terminal QR code
--help Show this help message

Config keys:
project-id WalletConnect Cloud project ID

Environment:
WALLETCONNECT_PROJECT_ID Required for connect and sign commands`);
WALLETCONNECT_PROJECT_ID Overrides config project-id when set`);
}

function getProjectId(): string {
const id = process.env.WALLETCONNECT_PROJECT_ID;
const id = resolveProjectId();
if (!id) {
console.error("Error: WALLETCONNECT_PROJECT_ID environment variable is required.");
console.error("Error: No project ID found. Set via: walletconnect config set project-id <id>");
process.exit(1);
}
return id;
Expand Down Expand Up @@ -102,12 +108,9 @@ async function cmdSign(message: string, browser: boolean): Promise<void> {
console.log();
}

const walletName = result.session.peer.metadata.name;
const { chain, address } = parseAccount(result.accounts[0]);
const hexMessage = "0x" + Buffer.from(message, "utf8").toString("hex");

console.log(`Requesting signature from ${walletName}...\n`);

const signature = await sdk.request<string>({
chainId: chain,
request: {
Expand Down Expand Up @@ -163,6 +166,32 @@ async function main(): Promise<void> {
case "disconnect":
await cmdDisconnect();
break;
case "config": {
const action = filtered[1];
const key = filtered[2];
if (action === "set") {
const value = filtered[3];
if (key === "project-id" && value) {
setConfigValue("projectId", value);
console.log(`Saved project-id to ~/.walletconnect-cli/config.json`);
} else {
console.error("Usage: walletconnect config set project-id <value>");
process.exit(1);
}
} else if (action === "get") {
if (key === "project-id") {
const value = getConfigValue("projectId");
console.log(value || "(not set)");
} else {
console.error("Usage: walletconnect config get project-id");
process.exit(1);
}
} else {
console.error("Usage: walletconnect config <set|get> <key> [value]");
process.exit(1);
}
break;
}
case "--help":
case "-h":
case undefined:
Expand All @@ -175,7 +204,10 @@ async function main(): Promise<void> {
}
}

main().catch((err) => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});
main().then(
() => process.exit(0),
(err) => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
},
);
53 changes: 49 additions & 4 deletions packages/cli-sdk/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EventEmitter } from "events";
import { execSync } from "child_process";
import { homedir } from "os";
import { join } from "path";
import { KeyValueStorage } from "@walletconnect/keyvaluestorage";
Expand Down Expand Up @@ -113,6 +114,8 @@
throw new Error("No active session. Call connect() first.");
}

this.logRequestDetails(options);

try {
return await client.request<T>({
topic,
Expand Down Expand Up @@ -142,14 +145,23 @@

try {
const client = await this.ensureClient();
await client.disconnect({
topic: this.currentSession.topic,
reason: { code: 6000, message: "User disconnected" },
});
// Absorb relay WebSocket errors during disconnect so they don't
// surface as unhandled 'error' events that crash the process.
const swallow = () => {};
client.core.relayer.on("error", swallow);
try {
await client.disconnect({
topic: this.currentSession.topic,
reason: { code: 6000, message: "User disconnected" },
});
} finally {
client.core.relayer.off("error", swallow);
}
} catch {
// Ignore disconnect errors — session may have already expired
}

// Always clean up local state even if relay notification failed
this.currentSession = null;
this.emit("disconnect");
}
Expand All @@ -175,6 +187,13 @@
this.browserUI = null;
}
this.removeAllListeners();
if (this.signClient) {
try {
await this.signClient.core.relayer.transportClose();
} catch {
// ignore cleanup errors
}
}
this.signClient = null;
this.currentSession = null;
}
Expand All @@ -201,6 +220,32 @@

// ---------- Private -------------------------------------------------- //

private logRequestDetails(options: RequestOptions): void {
const walletName = this.currentSession?.peer.metadata.name;
if (walletName) {
console.log(`\nRequesting approval on ${walletName}...`);
}

if (options.request.method === "eth_sendTransaction") {
const params = options.request.params as Array<{ data?: string }>;
const data = params[0]?.data;
if (data && data !== "0x") {
try {
const decoded = execSync(`cast 4d ${data}`, {

Check warning

Code scanning / CodeQL

Unsafe shell command constructed from library input Medium

This string concatenation which depends on
library input
is later used in a
shell command
.

Copilot Autofix

AI 30 days ago

In general, to fix this kind of issue you should avoid constructing shell commands by concatenating untrusted strings and passing them to an API that invokes a shell (exec/execSync with a single string). Instead, either (1) pass arguments as an array to a non-shell-spawning API such as execFile/execFileSync, or (2) if you must invoke a shell, properly escape any untrusted values using a library like shell-quote before embedding them in the command string.

The best fix here, without changing functionality, is to keep calling the cast CLI but pass it as a program with separate arguments, so that the untrusted data value is not interpreted by the shell. We can do this by importing execFileSync from child_process alongside execSync (leaving execSync untouched if used elsewhere) and replacing the unsafe execSync("cast 4d " + data, ...) call with execFileSync("cast", ["4d", data], ...). execFileSync does not invoke a shell and treats data as a literal argument to cast, eliminating command injection while preserving behavior (same program, same args, same options).

Concretely:

  • Update the import on line 2 in packages/cli-sdk/src/client.ts to also import execFileSync.
  • In logRequestDetails, replace the execSync invocation on lines 234–238 with an execFileSync call that passes "cast" as the command and ["4d", data] as the arguments, using the same options (encoding, timeout, stdio).

No additional helper methods are needed; only this import and call-site replacement are required.

Suggested changeset 1
packages/cli-sdk/src/client.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/cli-sdk/src/client.ts b/packages/cli-sdk/src/client.ts
--- a/packages/cli-sdk/src/client.ts
+++ b/packages/cli-sdk/src/client.ts
@@ -1,5 +1,5 @@
 import { EventEmitter } from "events";
-import { execSync } from "child_process";
+import { execSync, execFileSync } from "child_process";
 import { homedir } from "os";
 import { join } from "path";
 import { KeyValueStorage } from "@walletconnect/keyvaluestorage";
@@ -231,7 +231,7 @@
       const data = params[0]?.data;
       if (data && data !== "0x") {
         try {
-          const decoded = execSync(`cast 4d ${data}`, {
+          const decoded = execFileSync("cast", ["4d", data], {
             encoding: "utf-8",
             timeout: 5000,
             stdio: ["pipe", "pipe", "pipe"],
EOF
@@ -1,5 +1,5 @@
import { EventEmitter } from "events";
import { execSync } from "child_process";
import { execSync, execFileSync } from "child_process";
import { homedir } from "os";
import { join } from "path";
import { KeyValueStorage } from "@walletconnect/keyvaluestorage";
@@ -231,7 +231,7 @@
const data = params[0]?.data;
if (data && data !== "0x") {
try {
const decoded = execSync(`cast 4d ${data}`, {
const decoded = execFileSync("cast", ["4d", data], {
encoding: "utf-8",
timeout: 5000,
stdio: ["pipe", "pipe", "pipe"],
Copilot is powered by AI and may make mistakes. Always verify output.
encoding: "utf-8",
timeout: 5000,
stdio: ["pipe", "pipe", "pipe"],
}).trim();
if (decoded) {
console.log(`\n Decoded calldata:\n${decoded.split("\n").map((l) => ` ${l}`).join("\n")}\n`);
}
} catch {
// cast not available or decode failed — skip silently
}
}
}
}

private async ensureClient(): Promise<InstanceType<typeof SignClient>> {
if (this.signClient) return this.signClient;

Expand Down
Loading