Skip to content

# Command Injection Reproduction Notes for faucet_server.js - #1749

Open
DongZifan wants to merge 3 commits into
scrtlabs:masterfrom
DongZifan:master
Open

DongZifan wants to merge 3 commits into
scrtlabs:masterfrom
DongZifan:master

Conversation

@DongZifan

Copy link
Copy Markdown

Summary

Hello,
I am writing to report a potential OS Command Injection vulnerability in the following file:
deployment/docker/localsecret/faucet/faucet_server.js

The issue appears when the address query parameter of the /faucet endpoint is passed as the destination address into send_command(), which concatenates it into a shell command string that is later executed via child_process.exec(...). Under certain conditions, a specially crafted address value containing shell metacharacters (such as &, |, or ;) may allow unintended command execution on the host system. This poses a significant security risk as it allows for unauthenticated Remote Code Execution (RCE) on the faucet host.

Root Cause

The issue is caused by unsafe shell command construction in the following function:

const exec = require("child_process").exec;

function execShellCommand(cmd) {
  return new Promise((resolve, reject) => {
    exec(cmd, (error, stdout, stderr) => { ... });
  });
}

async function send_command(src_key_name, src_address, dest_address, amount) {
  const send_message = `secretd tx bank send ${src_address} ${dest_address} ${amount}${DENOM} --from ${src_key_name} --gas-prices 0.25uscrt -y`;
  const result = await execShellCommand(send_message);
  return result.txhash;
}

dest_address is attacker-controlled (address), and because exec(...) runs the command through the system shell, any shell metacharacters present in it are interpreted as command syntax rather than literal text:

} else if (req.url.startsWith("/faucet")) {
  ...
  const address = querystring.parse(req.url)["/faucet?address"];
  ...
  const txhash = await send_command(
    FAUCET_WALLET_NAME,
    faucet_address,
    address,          // <-- attacker-controlled
    FAUCET_AMOUNT
  );

Reproduction Material

A minimal reproduction script is provided in: poc_faucet_server.js
poc_faucet_server.js

This Proof of Concept (PoC) is intended to demonstrate that external input can reach dangerous command execution logic through the vulnerable code path.

What the PoC Does

The PoC performs a minimal end-to-end trigger of the vulnerable code path:

  1. It hooks child_process.exec to log intercepted commands before they execute.
  2. It stubs http so the request handler can be captured without binding a port.
  3. It loads faucet_server.js.
  4. It invokes the request handler with a crafted URL: /faucet?address=x%20%26%20calc, i.e. address = "x & calc".
  5. This causes the application to build secretd tx bank send <src> <address> <amount><DENOM> --from ... -y, where & is interpreted as a command separator and calc is executed as a second command.

In the provided example, the injected payload is crafted so that successful command execution launches the Calculator application. This serves as a visible indicator that external input can reach the OS command execution sink without proper sanitization.

Example Payload

The PoC uses the following payload for the address query parameter:

x & calc

How to Run

Run from the folder containing the files:

node poc_faucet_server.js

Expected Output

When the PoC runs, you should see output similar to:

[POC] Sending request: /faucet?address=x%20%26%20calc

[POC] child_process.exec() would run:
>> COMMAND: secretd keys list

[POC] child_process.exec() would run:
>> COMMAND: secretd tx bank send secret1faucetaddress000000000000000000000 x & calc 1000000000uscrt --from a --gas-prices 0.25uscrt -y
[POC] >>> INJECTION CONFIRMED: "calc" reached the shell command
[POC]     Executing the command for real ...
[POC] status: 200
[POC] response body: {"txhash":"POC_TXHASH"}

[POC] The injection instruction has been delivered to the operating system

In the vulnerable version, when the crafted address is processed, the shell interprets the & metacharacter and executes the injected calc command, launching the Calculator application as a benign demonstration effect.
This shows that the attacker-controlled address value can influence command execution behavior through the vulnerable send_command() path.

Patch Explanation

This branch also includes a patched version of faucet_server.js (new_faucet_server.js) intended to mitigate the command injection risk described above.

What the patch changes

The patch replaces child_process.exec() — which runs the command through a shell — with child_process.execFile(), which executes the program directly and passes each argument as a separate array element. This preserves the original behaviour (query the key list, send coins from the faucet) while ensuring address is never interpreted as shell syntax.

Validation introduced by the patch

execShellCommand now takes the program and its arguments separately, and callers build explicit argument arrays:

const execFile = require("child_process").execFile;

function execShellCommand(cmd, args = []) {
  return new Promise((resolve, reject) => {
    execFile(cmd, args, (error, stdout, stderr) => { ... });
  });
}

async function send_command(src_key_name, src_address, dest_address, amount) {
  const send_args = [
    "tx", "bank", "send",
    src_address,
    dest_address,
    `${amount}${DENOM}`,
    "--from", src_key_name,
    "--gas-prices", "0.25uscrt",
    "-y",
  ];
  const result = await execShellCommand("secretd", send_args);
  return result.txhash;
}

Because execFile does not invoke a shell, shell metacharacters such as &, |, ;, $(), and ` in address are treated as literal characters rather than command syntax. A value such as x & calc is delivered to secretd as a single literal argument and no second calc command is executed, while legitimate addresses continue to work as expected.

@DongZifan
DongZifan requested a review from liorbond as a code owner November 4, 2026 17:21
@DongZifan

Copy link
Copy Markdown
Author

Command Injection Reproduction Notes for faucet_server.js

Summary

Hello,
I am writing to report a potential OS Command Injection vulnerability in the following file:
scripts/faucet/faucet_server.js

The issue appears when the address query parameter of the /faucet endpoint is passed as the destination address into send_command(), which concatenates it into a shell command string that is later executed via child_process.exec(...). Under certain conditions, a specially crafted address value containing shell metacharacters (such as &, |, or ;) may allow unintended command execution on the host system. This poses a significant security risk as it allows for unauthenticated Remote Code Execution (RCE) on the faucet host.

Root Cause

The issue is caused by unsafe shell command construction in the following function:

const exec = require("child_process").exec;

function execShellCommand(cmd) {
  return new Promise((resolve, reject) => {
    exec(cmd, (error, stdout, stderr) => { ... });
  });
}

async function send_command(src_key_name, src_address, dest_address, amount) {
  const send_message = `secretd tx bank send ${src_address} ${dest_address} ${amount}${DENOM} --from ${src_key_name} --gas-prices 0.25uscrt -y 2> /dev/null`;
  const result = await execShellCommand(send_message);
  return result.txhash;
}

dest_address is attacker-controlled (address), and because exec(...) runs the command through the system shell, any shell metacharacters present in it are interpreted as command syntax rather than literal text:

} else if (req.url.startsWith("/faucet")) {
  ...
  const address = querystring.parse(req.url)["/faucet?address"];
  ...
  const txhash = await send_command(
    FAUCET_WALLET_NAME,
    faucet_address,
    address,          // <-- attacker-controlled
    FAUCET_AMOUNT
  );

Reproduction Material

A minimal reproduction script is provided in: poc_faucet_server.js
poc_faucet_server.js

This Proof of Concept (PoC) is intended to demonstrate that external input can reach dangerous command execution logic through the vulnerable code path.

What the PoC Does

The PoC performs a minimal end-to-end trigger of the vulnerable code path:

  1. It hooks child_process.exec to log intercepted commands before they execute.
  2. It stubs http so the request handler can be captured without binding a port.
  3. It loads faucet_server.js.
  4. It invokes the request handler with a crafted URL: /faucet?address=x%20%26%20calc%20%26, i.e. address = "x & calc &".
  5. This causes the application to build secretd tx bank send <src> <address> <amount><DENOM> --from ... -y 2> /dev/null, where & is interpreted as a command separator and calc is executed as a second command.

In the provided example, the injected payload is crafted so that successful command execution launches the Calculator application. This serves as a visible indicator that external input can reach the OS command execution sink without proper sanitization.

Example Payload

The PoC uses the following payload for the address query parameter:

x & calc &

How to Run

Run from the folder containing the files:

node poc_faucet_server.js

Expected Output

When the PoC runs, you should see output similar to:

[POC] Sending request: /faucet?address=x%20%26%20calc%20%26

[POC] child_process.exec() would run:
>> COMMAND: secretd keys list --output json 2> /dev/null

[POC] child_process.exec() would run:
>> COMMAND: secretd tx bank send secret1faucetaddress000000000000000000000 x & calc & 1000000000uscrt --from a --gas-prices 0.25uscrt -y 2> /dev/null
[POC] >>> INJECTION CONFIRMED: "calc" reached the shell command
[POC]     Executing the command for real ...
[POC] status: 200
[POC] response body: {"txhash":"POC_TXHASH"}

[POC] The injection instruction has been delivered to the operating system

In the vulnerable version, when the crafted address is processed, the shell interprets the & metacharacter and executes the injected calc command, launching the Calculator application as a benign demonstration effect.
This shows that the attacker-controlled address value can influence command execution behavior through the vulnerable send_command() path.

Patch Explanation

This branch also includes a patched version of faucet_server.js (new_faucet_server.js) intended to mitigate the command injection risk described above.

What the patch changes

The patch replaces child_process.exec() — which runs the command through a shell — with child_process.execFile(), which executes the program directly and passes each argument as a separate array element. This preserves the original behaviour (query the key list, send coins from the faucet) while ensuring address is never interpreted as shell syntax.

Because execFile does not use a shell, the 2> /dev/null redirection in the original commands cannot be expressed as a shell operator. Its effect (stderr being ignored) is preserved by simply not acting on stderr in the callback — the original code already neutralised its own else if (stderr) branch via that redirection.

Validation introduced by the patch

execShellCommand now takes the program and its arguments separately, and callers build explicit argument arrays:

const execFile = require("child_process").execFile;

function execShellCommand(cmd, args = []) {
  return new Promise((resolve, reject) => {
    execFile(cmd, args, (error, stdout) => {
      if (error) {
        reject(error);
      } else {
        resolve(JSON.parse(stdout));  // stderr is ignored (see note above)
      }
    });
  });
}

async function send_command(src_key_name, src_address, dest_address, amount) {
  const send_args = [
    "tx", "bank", "send",
    src_address,
    dest_address,
    `${amount}${DENOM}`,
    "--from", src_key_name,
    "--gas-prices", "0.25uscrt",
    "-y",
  ];
  const result = await execShellCommand("secretd", send_args);
  return result.txhash;
}

Because execFile does not invoke a shell, shell metacharacters such as &, |, ;, $(), and ` in address are treated as literal characters rather than command syntax. A value such as x & calc & is delivered to secretd as a single literal argument and no second calc command is executed, while legitimate addresses continue to work as expected.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant