Shell.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. <?php
  2. /**
  3. * Class used for executing shell commands
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. namespace MediaWiki\Shell;
  23. use Hooks;
  24. use MediaWiki\MediaWikiServices;
  25. /**
  26. * Executes shell commands
  27. *
  28. * @since 1.30
  29. *
  30. * Use call chaining with this class for expressiveness:
  31. * $result = Shell::command( 'some command' )
  32. * ->input( 'foo' )
  33. * ->environment( [ 'ENVIRONMENT_VARIABLE' => 'VALUE' ] )
  34. * ->limits( [ 'time' => 300 ] )
  35. * ->execute();
  36. *
  37. * ... = $result->getExitCode();
  38. * ... = $result->getStdout();
  39. * ... = $result->getStderr();
  40. */
  41. class Shell {
  42. /**
  43. * Disallow any root access. Any setuid binaries
  44. * will be run without elevated access.
  45. *
  46. * @since 1.31
  47. */
  48. const NO_ROOT = 1;
  49. /**
  50. * Use seccomp to block dangerous syscalls
  51. * @see <https://en.wikipedia.org/wiki/seccomp>
  52. *
  53. * @since 1.31
  54. */
  55. const SECCOMP = 2;
  56. /**
  57. * Create a private /dev
  58. *
  59. * @since 1.31
  60. */
  61. const PRIVATE_DEV = 4;
  62. /**
  63. * Restrict the request to have no
  64. * network access
  65. *
  66. * @since 1.31
  67. */
  68. const NO_NETWORK = 8;
  69. /**
  70. * Deny execve syscall with seccomp
  71. * @see <https://en.wikipedia.org/wiki/exec_(system_call)>
  72. *
  73. * @since 1.31
  74. */
  75. const NO_EXECVE = 16;
  76. /**
  77. * Deny access to LocalSettings.php (MW_CONFIG_FILE)
  78. *
  79. * @since 1.31
  80. */
  81. const NO_LOCALSETTINGS = 32;
  82. /**
  83. * Apply a default set of restrictions for improved
  84. * security out of the box.
  85. *
  86. * @note This value will change over time to provide increased security
  87. * by default, and is not guaranteed to be backwards-compatible.
  88. * @since 1.31
  89. */
  90. const RESTRICT_DEFAULT = self::NO_ROOT | self::SECCOMP | self::PRIVATE_DEV |
  91. self::NO_LOCALSETTINGS;
  92. /**
  93. * Don't apply any restrictions
  94. *
  95. * @since 1.31
  96. */
  97. const RESTRICT_NONE = 0;
  98. /**
  99. * Returns a new instance of Command class
  100. *
  101. * @note You should check Shell::isDisabled() before calling this
  102. * @param string|string[] ...$commands String or array of strings representing the command to
  103. * be executed, each value will be escaped.
  104. * Example: [ 'convert', '-font', 'font name' ] would produce "'convert' '-font' 'font name'"
  105. * @return Command
  106. */
  107. public static function command( ...$commands ): Command {
  108. if ( count( $commands ) === 1 && is_array( reset( $commands ) ) ) {
  109. // If only one argument has been passed, and that argument is an array,
  110. // treat it as a list of arguments
  111. $commands = reset( $commands );
  112. }
  113. $command = MediaWikiServices::getInstance()
  114. ->getShellCommandFactory()
  115. ->create();
  116. return $command->params( $commands );
  117. }
  118. /**
  119. * Check if this class is effectively disabled via php.ini config
  120. *
  121. * @return bool
  122. */
  123. public static function isDisabled() {
  124. static $disabled = null;
  125. if ( is_null( $disabled ) ) {
  126. if ( !function_exists( 'proc_open' ) ) {
  127. wfDebug( "proc_open() is disabled\n" );
  128. $disabled = true;
  129. } else {
  130. $disabled = false;
  131. }
  132. }
  133. return $disabled;
  134. }
  135. /**
  136. * Version of escapeshellarg() that works better on Windows.
  137. *
  138. * Originally, this fixed the incorrect use of single quotes on Windows
  139. * (https://bugs.php.net/bug.php?id=26285) and the locale problems on Linux in
  140. * PHP 5.2.6+ (bug backported to earlier distro releases of PHP).
  141. *
  142. * @param string|string[] ...$args strings to escape and glue together, or a single
  143. * array of strings parameter. Null values are ignored.
  144. * @return string
  145. */
  146. public static function escape( ...$args ) {
  147. if ( count( $args ) === 1 && is_array( reset( $args ) ) ) {
  148. // If only one argument has been passed, and that argument is an array,
  149. // treat it as a list of arguments
  150. $args = reset( $args );
  151. }
  152. $first = true;
  153. $retVal = '';
  154. foreach ( $args as $arg ) {
  155. if ( $arg === null ) {
  156. continue;
  157. }
  158. if ( !$first ) {
  159. $retVal .= ' ';
  160. } else {
  161. $first = false;
  162. }
  163. if ( wfIsWindows() ) {
  164. // Escaping for an MSVC-style command line parser and CMD.EXE
  165. // Refs:
  166. // * https://web.archive.org/web/20020708081031/http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html
  167. // * https://technet.microsoft.com/en-us/library/cc723564.aspx
  168. // * T15518
  169. // * CR r63214
  170. // Double the backslashes before any double quotes. Escape the double quotes.
  171. $tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE );
  172. $arg = '';
  173. $iteration = 0;
  174. foreach ( $tokens as $token ) {
  175. if ( $iteration % 2 == 1 ) {
  176. // Delimiter, a double quote preceded by zero or more slashes
  177. $arg .= str_replace( '\\', '\\\\', substr( $token, 0, -1 ) ) . '\\"';
  178. } elseif ( $iteration % 4 == 2 ) {
  179. // ^ in $token will be outside quotes, need to be escaped
  180. $arg .= str_replace( '^', '^^', $token );
  181. } else { // $iteration % 4 == 0
  182. // ^ in $token will appear inside double quotes, so leave as is
  183. $arg .= $token;
  184. }
  185. $iteration++;
  186. }
  187. // Double the backslashes before the end of the string, because
  188. // we will soon add a quote
  189. $m = [];
  190. if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) ) {
  191. $arg = $m[1] . str_replace( '\\', '\\\\', $m[2] );
  192. }
  193. // Add surrounding quotes
  194. $retVal .= '"' . $arg . '"';
  195. } else {
  196. $retVal .= escapeshellarg( $arg );
  197. }
  198. }
  199. return $retVal;
  200. }
  201. /**
  202. * Generate a Command object to run a MediaWiki CLI script.
  203. * Note that $parameters should be a flat array and an option with an argument
  204. * should consist of two consecutive items in the array (do not use "--option value").
  205. *
  206. * @note You should check Shell::isDisabled() before calling this
  207. * @param string $script MediaWiki CLI script with full path
  208. * @param string[] $parameters Arguments and options to the script
  209. * @param array $options Associative array of options:
  210. * 'php': The path to the php executable
  211. * 'wrapper': Path to a PHP wrapper to handle the maintenance script
  212. * @phan-param array{php?:string,wrapper?:string} $options
  213. * @return Command
  214. */
  215. public static function makeScriptCommand( $script, $parameters, $options = [] ): Command {
  216. global $wgPhpCli;
  217. // Give site config file a chance to run the script in a wrapper.
  218. // The caller may likely want to call wfBasename() on $script.
  219. Hooks::run( 'wfShellWikiCmd', [ &$script, &$parameters, &$options ] );
  220. $cmd = [ $options['php'] ?? $wgPhpCli ];
  221. if ( isset( $options['wrapper'] ) ) {
  222. $cmd[] = $options['wrapper'];
  223. }
  224. $cmd[] = $script;
  225. return self::command( $cmd )
  226. ->params( $parameters )
  227. ->restrict( self::RESTRICT_DEFAULT & ~self::NO_LOCALSETTINGS );
  228. }
  229. }