From e9a2b30744a62268c66d6c17730ed96486d9783c Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Fri, 18 Feb 2022 06:46:04 -0600 Subject: [PATCH] Enhanced RDoc concerning command injection (#5537) Clarifies security vulnerabilities for commands. Treats: Kernel.system Kernel.` (backtick) IO.popen IO.read IO.write IO.binread IO.binwrite IO.readlines IO.foreach --- doc/command_injection.rdoc | 29 ++++++++++++++++ io.c | 71 ++++++++++++++++++++++++-------------- object.c | 2 +- process.c | 3 ++ 4 files changed, 79 insertions(+), 26 deletions(-) create mode 100644 doc/command_injection.rdoc diff --git a/doc/command_injection.rdoc b/doc/command_injection.rdoc new file mode 100644 index 0000000000..8f1303bcf7 --- /dev/null +++ b/doc/command_injection.rdoc @@ -0,0 +1,29 @@ +== Command Injection + +Some Ruby core methods accept string data +that includes text to be executed as a system command. + +They should not be called with unknown or unsanitized commands. + +These methods include: + +- Kernel.system +- {`command` (backtick method)}[rdoc-ref:Kernel#`] + (also called by the expression %x[command]). +- IO.popen(command). +- IO.read(command). +- IO.write(command). +- IO.binread(command). +- IO.binwrite(command). +- IO.readlines(command). +- IO.foreach(command). + +Note that some of these methods do not execute commands when called +from subclass \File: + +- File.read(path). +- File.write(path). +- File.binread(path). +- File.binwrite(path). +- File.readlines(path). +- File.foreach(path). diff --git a/io.c b/io.c index 88897960ab..24488d2db7 100644 --- a/io.c +++ b/io.c @@ -7374,6 +7374,9 @@ static VALUE popen_finish(VALUE port, VALUE klass); * Executes the given command +cmd+ as a subprocess * whose $stdin and $stdout are connected to a new stream +io+. * + * This method has potential security vulnerabilities if called with untrusted input; + * see {Command Injection}[command_injection.rdoc]. + * * If no block is given, returns the new stream, * which depending on given +mode+ may be open for reading, writing, or both. * The stream should be explicitly closed (eventually) to avoid resource leaks. @@ -9813,10 +9816,15 @@ argf_readlines(int argc, VALUE *argv, VALUE argf) /* * call-seq: - * `cmd` -> string + * `command` -> string * - * Returns the $stdout output fromm running +cmd+ in a subshell; - * sets global variable $? to the process status: + * Returns the $stdout output from running +command+ in a subshell; + * sets global variable $? to the process status. + * + * This method has potential security vulnerabilities if called with untrusted input; + * see {Command Injection}[command_injection.rdoc]. + * + * Examples: * * $ `date` # => "Wed Apr 9 08:56:30 CDT 2003\n" * $ `echo oops && exit 99` # => "oops\n" @@ -11196,36 +11204,29 @@ io_s_foreach(VALUE v) /* * call-seq: - * IO.foreach(command, sep = $/, **opts) {|line| block } -> nil - * IO.foreach(command, limit, **opts) {|line| block } -> nil - * IO.foreach(command, sep, limit, **opts) {|line| block } -> nil * IO.foreach(path, sep = $/, **opts) {|line| block } -> nil * IO.foreach(path, limit, **opts) {|line| block } -> nil * IO.foreach(path, sep, limit, **opts) {|line| block } -> nil + * IO.foreach(command, sep = $/, **opts) {|line| block } -> nil + * IO.foreach(command, limit, **opts) {|line| block } -> nil + * IO.foreach(command, sep, limit, **opts) {|line| block } -> nil * IO.foreach(...) -> an_enumerator * * Calls the block with each successive line read from the stream. * - * The first argument must be a string; - * its meaning depends on whether it starts with the pipe character ('|'): + * When called from class \IO (but not subclasses of \IO), + * this method has potential security vulnerabilities if called with untrusted input; + * see {Command Injection}[command_injection.rdoc]. * - * - If so (and if +self+ is \IO), + * The first argument must be a string that is one of the following: + * + * - Path: if +self+ is a subclass of \IO (\File, for example), + * or if the string _does_ _not_ start with the pipe character ('|'), + * the string is the path to a file. + * - Command: if +self+ is the class \IO, + * and if the string starts with the pipe character, * the rest of the string is a command to be executed as a subprocess. - * - Otherwise, the string is the path to a file. - * - * With only argument +command+ given, executes the command in a shell, - * parses its $stdout into lines, as determined by the default line separator, - * and calls the block with each successive line: - * - * IO.foreach('| cat t.txt') {|line| p line } - * - * Output: - * - * "First line\n" - * "Second line\n" - * "\n" - * "Third line\n" - * "Fourth line\n" + * See the {Note on Security}[@Note+on+Security]. * * With only argument +path+ given, parses lines from the file at the given +path+, * as determined by the default line separator, @@ -11327,6 +11328,10 @@ io_s_readlines(VALUE v) * * Returns an array of all lines read from the stream. * + * When called from class \IO (but not subclasses of \IO), + * this method has potential security vulnerabilities if called with untrusted input; + * see {Command Injection}[command_injection.rdoc]. + * * The first argument must be a string; * its meaning depends on whether it starts with the pipe character ('|'): * @@ -11427,6 +11432,10 @@ seek_before_access(VALUE argp) * Opens the stream, reads and returns some or all of its content, * and closes the stream; returns +nil+ if no bytes were read. * + * When called from class \IO (but not subclasses of \IO), + * this method has potential security vulnerabilities if called with untrusted input; + * see {Command Injection}[command_injection.rdoc]. + * * The first argument must be a string; * its meaning depends on whether it starts with the pipe character ('|'): * @@ -11498,6 +11507,10 @@ rb_io_s_read(int argc, VALUE *argv, VALUE io) * Behaves like IO.read, except that the stream is opened in binary mode * with ASCII-8BIT encoding. * + * When called from class \IO (but not subclasses of \IO), + * this method has potential security vulnerabilities if called with untrusted input; + * see {Command Injection}[command_injection.rdoc]. + * */ static VALUE @@ -11599,10 +11612,14 @@ io_s_write(int argc, VALUE *argv, VALUE klass, int binary) * Opens the stream, writes the given +data+ to it, * and closes the stream; returns the number of bytes written. * + * When called from class \IO (but not subclasses of \IO), + * this method has potential security vulnerabilities if called with untrusted input; + * see {Command Injection}[command_injection.rdoc]. + * * The first argument must be a string; * its meaning depends on whether it starts with the pipe character ('|'): * - * - If so (and if +self+ is an instance of \IO), + * - If so (and if +self+ is \IO), * the rest of the string is a command to be executed as a subprocess. * - Otherwise, the string is the path to a file. * @@ -11660,6 +11677,10 @@ rb_io_s_write(int argc, VALUE *argv, VALUE io) * Behaves like IO.write, except that the stream is opened in binary mode * with ASCII-8BIT encoding. * + * When called from class \IO (but not subclasses of \IO), + * this method has potential security vulnerabilities if called with untrusted input; + * see {Command Injection}[command_injection.rdoc]. + * */ static VALUE diff --git a/object.c b/object.c index 33715a5714..f056767f13 100644 --- a/object.c +++ b/object.c @@ -4389,7 +4389,7 @@ InitVM_Object(void) * * === Subprocesses * - * - #`cmd`:: Returns the standard output of running +cmd+ in a subshell. + * - #`command`:: Returns the standard output of running +command+ in a subshell. * - #exec:: Replaces current process with a new process. * - #fork:: Forks the current process into two processes. * - #spawn:: Executes the given command and returns its pid without waiting diff --git a/process.c b/process.c index 8eee6f772a..4011aa569f 100644 --- a/process.c +++ b/process.c @@ -4755,6 +4755,9 @@ rb_spawn(int argc, const VALUE *argv) * Executes _command..._ in a subshell. * _command..._ is one of following forms. * + * This method has potential security vulnerabilities if called with untrusted input; + * see {Command Injection}[command_injection.rdoc]. + * * [commandline] * command line string which is passed to the standard shell * [cmdname, arg1, ...]