logo

Database

Need

Prevention of OS command injection by avoiding shells and validating program arguments

Context

• Usage of Elixir 1.15+ for building application services

• Usage of Plug.Router for handling HTTP requests

• Usage of System.cmd/3 for running external programs

Description

1. Non compliant code

defmodule MyAppWeb.ToolsRouter do
  use Plug.Router

  plug :match
  plug :fetch_query_params
  plug :dispatch

  get "/ping" do...

The `/ping` route below inserts the `host` query parameter into a command line with string interpolation and runs it with `System.shell/1`. `System.shell/1` passes the whole string to the operating system shell, so metacharacters in the parameter start new commands. A request to `/ping?host=127.0.0.1;cat /etc/passwd` runs `cat` with the privileges of the application, and payloads with `$(...)`, backticks, `|` or `&&` work the same way. The output of the injected command is returned in the response. `:os.cmd/1` also runs its argument through a shell and has the same problem. Passing request data as the program name of `System.cmd/3`, for example `System.cmd(conn.params["tool"], [])`, lets the caller run any executable on the host.

2. Steps

• Do not call `System.shell/1` or `:os.cmd/1` with strings that contain request data.

• Run external programs with `System.cmd/3`, passing a fixed program name and each value as a separate element of the argument list.

• Validate every request-derived argument against a strict format, such as parsing it with `:inet.parse_address/1`, before it reaches the command.

• Reject values that start with `-`, and place user input after a `--` separator when the program supports it.

• Prefer an Elixir or Erlang library over an external program when one exists.

• Run the application with an operating system user that has only the permissions it needs.

3. Secure code example

defmodule MyAppWeb.ToolsRouter do
  use Plug.Router

  plug :match
  plug :fetch_query_params
  plug :dispatch

  get "/ping" do...

The corrected route runs `ping` with `System.cmd/3`, which starts the program directly, without a shell, and passes each element of the argument list to it unchanged. Characters such as `;`, `|` or `$(` have no special meaning. Before that, the `host` parameter is parsed with `:inet.parse_address/1`, which only accepts a valid IPv4 or IPv6 address. That rejects shell metacharacters and also values that start with `-`, so the caller cannot inject options such as `-f` into `ping`. The command receives the normalized address produced by `:inet.ntoa/1`, not the raw request string. When a program must receive free-form input, the same pattern applies: validate the value against a strict format, pass it as a separate argument, and place it after a `--` separator when the program supports it.