Skip to content

Prevent shell command injection in psql --options#4204

Open
tas50 wants to merge 1 commit into
chef:mainfrom
tas50:fix-csc-psql-command-injection
Open

Prevent shell command injection in psql --options#4204
tas50 wants to merge 1 commit into
chef:mainfrom
tas50:fix-csc-psql-command-injection

Conversation

@tas50

@tas50 tas50 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Problem

The psql command in src/chef-server-ctl/plugins/psql.rb builds a single command string and executes it with exec cmd:

if ARGV.include?(options_arg)
  psql_options = " #{ARGV[ARGV.index(options_arg) + 1]}"
end
...
cmd = "/opt/opscode/embedded/bin/psql --host #{db_host} --username #{db_username} --port #{db_port} --dbname #{db_name}#{psql_options}"
exec cmd

When exec is given a single string that contains shell metacharacters, Ruby runs it through /bin/sh. The user-supplied --options value is interpolated into that string unescaped, so it is interpreted by the shell. For example:

chef-server-ctl psql opscode_chef --options '; <command>'

executes the injected command rather than passing it to psql as an option.

Fix

Build an explicit argument vector and call exec(*cmd). With multiple arguments Ruby does not invoke a shell, so arguments are passed to psql verbatim. The --options value is tokenized with Shellwords.split (honoring shell-style quoting) and each token becomes a literal argv element, preserving the ability to pass real psql options without allowing shell interpretation:

psql_options = []
if ARGV.include?(options_arg)
  psql_options = Shellwords.split(ARGV[ARGV.index(options_arg) + 1].to_s)
end
...
cmd = ["/opt/opscode/embedded/bin/psql",
       "--host", db_host.to_s,
       "--username", db_username.to_s,
       "--port", db_port.to_s,
       "--dbname", db_name.to_s,
       *psql_options]
exec(*cmd)

Note: the chef-server-ctl gem bundle targets Ruby 3.1 and could not be installed in my local environment (Ruby 4.x), so I verified the change with ruby -c and am relying on CI for the rspec suite.

The psql command built a single command string and ran it with
`exec cmd`. Because the string contained the user-supplied --options
value and was passed to exec as one string, Ruby executed it through a
shell, so shell metacharacters in --options were interpreted. For
example:

    chef-server-ctl psql opscode_chef --options '; <command>'

would run the injected command instead of treating it as a psql option.

Build an explicit argument vector and call exec(*cmd) so no shell is
involved. The --options value is tokenized with Shellwords.split and
each token is passed to psql as a literal argument, preserving the
ability to pass real psql options without allowing shell interpretation.

Signed-off-by: Tim Smith <tsmith84@proton.me>
@tas50 tas50 requested review from a team as code owners June 6, 2026 16:22
@netlify

netlify Bot commented Jun 6, 2026

Copy link
Copy Markdown

👷 Deploy Preview for chef-server processing.

Name Link
🔨 Latest commit f2a14ce
🔍 Latest deploy log https://app.netlify.com/projects/chef-server/deploys/6a2449384982e300086e21e1

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