Merge "Chinese Conversion Table Update 2017-6"
[lhc/web/wiklou.git] / includes / shell / FirejailCommand.php
1 <?php
2 /**
3 * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org>
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 *
19 */
20
21 namespace MediaWiki\Shell;
22
23 use RuntimeException;
24
25 /**
26 * Restricts execution of shell commands using firejail
27 *
28 * @see https://firejail.wordpress.com/
29 * @since 1.31
30 */
31 class FirejailCommand extends Command {
32
33 /**
34 * @var string Path to firejail
35 */
36 private $firejail;
37
38 /**
39 * @var string[]
40 */
41 private $whitelistedPaths = [];
42
43 /**
44 * @param string $firejail Path to firejail
45 */
46 public function __construct( $firejail ) {
47 parent::__construct();
48 $this->firejail = $firejail;
49 }
50
51 /**
52 * @inheritDoc
53 */
54 public function whitelistPaths( array $paths ) {
55 $this->whitelistedPaths = array_merge( $this->whitelistedPaths, $paths );
56 return $this;
57 }
58
59 /**
60 * @inheritDoc
61 */
62 protected function buildFinalCommand( $command ) {
63 // If there are no restrictions, don't use firejail
64 if ( $this->restrictions === 0 ) {
65 $splitCommand = explode( ' ', $command, 2 );
66 $this->logger->debug(
67 "firejail: Command {$splitCommand[0]} {params} has no restrictions",
68 [ 'params' => isset( $splitCommand[1] ) ? $splitCommand[1] : '' ]
69 );
70 return parent::buildFinalCommand( $command );
71 }
72
73 if ( $this->firejail === false ) {
74 throw new RuntimeException( 'firejail is enabled, but cannot be found' );
75 }
76 // quiet has to come first to prevent firejail from adding
77 // any output.
78 $cmd = [ $this->firejail, '--quiet' ];
79 // Use a profile that allows people to add local overrides
80 // if their system is setup in an incompatible manner. Also it
81 // prevents any default profiles from running.
82 // FIXME: Doesn't actually override command-line switches?
83 $cmd[] = '--profile=' . __DIR__ . '/firejail.profile';
84
85 // By default firejail hides all other user directories, so if
86 // MediaWiki is inside a home directory (/home) but not the
87 // current user's home directory, pass --allusers to whitelist
88 // the home directories again.
89 static $useAllUsers = null;
90 if ( $useAllUsers === null ) {
91 global $IP;
92 // In case people are doing funny things with symlinks
93 // or relative paths, resolve them all.
94 $realIP = realpath( $IP );
95 $currentUser = posix_getpwuid( posix_geteuid() );
96 $useAllUsers = ( strpos( $realIP, '/home/' ) === 0 )
97 && ( strpos( $realIP, $currentUser['dir'] ) !== 0 );
98 if ( $useAllUsers ) {
99 $this->logger->warning( 'firejail: MediaWiki is located ' .
100 'in a home directory that does not belong to the ' .
101 'current user, so allowing access to all home ' .
102 'directories (--allusers)' );
103 }
104 }
105
106 if ( $useAllUsers ) {
107 $cmd[] = '--allusers';
108 }
109
110 if ( $this->whitelistedPaths ) {
111 // Always whitelist limit.sh
112 $cmd[] = '--whitelist=' . __DIR__ . '/limit.sh';
113 foreach ( $this->whitelistedPaths as $whitelistedPath ) {
114 $cmd[] = "--whitelist={$whitelistedPath}";
115 }
116 }
117
118 if ( $this->hasRestriction( Shell::NO_LOCALSETTINGS ) ) {
119 $cmd[] = '--blacklist=' . realpath( MW_CONFIG_FILE );
120 }
121
122 if ( $this->hasRestriction( Shell::NO_ROOT ) ) {
123 $cmd[] = '--noroot';
124 }
125
126 $seccomp = [];
127
128 if ( $this->hasRestriction( Shell::SECCOMP ) ) {
129 $seccomp[] = '@default';
130 }
131
132 if ( $this->hasRestriction( Shell::NO_EXECVE ) ) {
133 $seccomp[] = 'execve';
134 // Normally firejail will run commands in a bash shell,
135 // but that won't work if we ban the execve syscall, so
136 // run the command without a shell.
137 $cmd[] = '--shell=none';
138 }
139
140 if ( $seccomp ) {
141 $cmd[] = '--seccomp=' . implode( ',', $seccomp );
142 }
143
144 if ( $this->hasRestriction( Shell::PRIVATE_DEV ) ) {
145 $cmd[] = '--private-dev';
146 }
147
148 if ( $this->hasRestriction( Shell::NO_NETWORK ) ) {
149 $cmd[] = '--net=none';
150 }
151
152 $builtCmd = implode( ' ', $cmd );
153
154 // Prefix the firejail command in front of the wanted command
155 return parent::buildFinalCommand( "$builtCmd -- {$command}" );
156 }
157
158 }