2 suPHP - (c)2002-2008 Sebastian Marsching <sebastian@marsching.com>
4 This file is part of suPHP.
6 suPHP is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 suPHP is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with suPHP; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 #include "CommandLine.hpp"
26 #include "Environment.hpp"
27 #include "Exception.hpp"
29 #include "Configuration.hpp"
31 #include "API_Helper.hpp"
33 #include "UserInfo.hpp"
34 #include "GroupInfo.hpp"
36 #include "PathMatcher.hpp"
38 #include "Application.hpp"
40 using namespace suPHP
;
43 suPHP::Application::Application() {
48 int suPHP::Application::run(CommandLine
& cmdline
, Environment
& env
) {
50 API
& api
= API_Helper::getSystemAPI();
51 Logger
& logger
= api
.getSystemLogger();
54 File cfgFile
= File(OPT_CONFIGFILE
);
56 File cfgFile
= File("/etc/suphp.conf");
59 std::string interpreter
;
60 TargetMode targetMode
;
63 // Begin try block - soft exception cannot really be handled before
66 std::string scriptFilename
;
68 GroupInfo targetGroup
;
70 // If caller is super-user, print info message and exit
71 if (api
.getRealProcessUser().isSuperUser()) {
72 this->printAboutMessage();
75 config
.readFromFile(cfgFile
);
77 // Check permissions (real uid, effective uid)
78 this->checkProcessPermissions(config
);
81 // not done before, because we need super-user privileges for
86 scriptFilename
= env
.getVar("SCRIPT_FILENAME");
87 } catch (KeyNotFoundException
& e
) {
88 logger
.logError("Environment variable SCRIPT_FILENAME not set");
89 this->printAboutMessage();
94 // Do checks that do not need target user info
95 this->checkScriptFileStage1(scriptFilename
, config
, env
);
97 // Find out target user
98 this->checkProcessPermissions(scriptFilename
, config
, env
, targetUser
, targetGroup
);
100 // Now do checks that might require user info
101 this->checkScriptFileStage2(scriptFilename
, config
, env
, targetUser
, targetGroup
);
103 // Root privileges are needed for chroot()
104 // so do this before changing process permissions
105 if (config
.getChrootPath().length() > 0) {
106 PathMatcher pathMatcher
= PathMatcher(targetUser
, targetGroup
);
107 std::string chrootPath
= pathMatcher
.resolveVariables(config
.getChrootPath());
108 api
.chroot(chrootPath
);
111 this->changeProcessPermissions(config
, targetUser
, targetGroup
);
113 interpreter
= this->getInterpreter(env
, config
);
114 targetMode
= this->getTargetMode(interpreter
);
116 // Prepare environment for new process
117 newEnv
= this->prepareEnvironment(env
, config
, targetMode
);
119 // Set PATH_TRANSLATED to SCRIPT_FILENAME, otherwise
120 // the PHP interpreter will not be able to find the script
121 if (targetMode
== TARGETMODE_PHP
&& newEnv
.hasVar("PATH_TRANSLATED")) {
122 newEnv
.setVar("PATH_TRANSLATED", scriptFilename
);
125 // Log attempt to execute script
126 logger
.logInfo("Executing \"" + scriptFilename
+ "\" as UID "
127 + Util::intToStr(api
.getEffectiveProcessUser().getUid())
130 api
.getEffectiveProcessGroup().getGid()));
132 this->executeScript(scriptFilename
, interpreter
, targetMode
, newEnv
,
135 // Function should never return
136 // So, if we get here, return with error code
138 } catch (SoftException
& e
) {
139 if (!config
.getErrorsToBrowser()) {
143 std::cout
<< "Content-Type: text/html\n"
148 << " <title>500 Internal Server Error</title>\n"
151 << " <h1>Internal Server Error</h1>\n"
152 << " <p>" << e
.getMessage() << "</p>\n"
154 << " <address>suPHP " << PACKAGE_VERSION
<< "</address>\n"
161 void suPHP::Application::printAboutMessage() {
162 std::cerr
<< "suPHP version " << PACKAGE_VERSION
<< "\n";
163 std::cerr
<< "(c) 2002-2007 Sebastian Marsching\n";
164 std::cerr
<< std::endl
;
165 std::cerr
<< "suPHP has to be called by mod_suphp to work." << std::endl
;
169 void suPHP::Application::checkProcessPermissions(Configuration
& config
)
170 throw (SecurityException
, LookupException
) {
171 API
& api
= API_Helper::getSystemAPI();
172 if (api
.getRealProcessUser() !=
173 api
.getUserInfo(config
.getWebserverUser())) {
174 throw SecurityException("Calling user is not webserver user!",
178 if (!api
.getEffectiveProcessUser().isSuperUser()) {
179 throw SecurityException(
180 "Do not have root privileges. Executable not set-uid root?",
186 void suPHP::Application::checkScriptFileStage1(
187 const std::string
& scriptFilename
,
188 const Configuration
& config
,
189 const Environment
& environment
) const
190 throw (SystemException
, SoftException
) {
191 Logger
& logger
= API_Helper::getSystemAPI().getSystemLogger();
192 File scriptFile
= File(scriptFilename
);
193 File realScriptFile
= File(scriptFile
.getRealPath());
195 // Check wheter file exists
196 if (!scriptFile
.exists()) {
197 std::string error
= "File " + scriptFile
.getPath() + " does not exist";
198 logger
.logWarning(error
);
199 throw SoftException(error
, __FILE__
, __LINE__
);
201 if (!realScriptFile
.exists()) {
202 std::string error
= "File " + realScriptFile
.getPath()
203 + " referenced by symlink " +scriptFile
.getPath()
205 logger
.logWarning(error
);
206 throw SoftException(error
, __FILE__
, __LINE__
);
209 // If enabled, check whether script is in the vhost's docroot
210 if (!environment
.hasVar("DOCUMENT_ROOT"))
211 throw SoftException("Environment variable DOCUMENT_ROOT not set",
213 if (config
.getCheckVHostDocroot()
214 && realScriptFile
.getPath().find(environment
.getVar("DOCUMENT_ROOT"))
217 std::string error
= "File \"" + realScriptFile
.getPath()
218 + "\" is not in document root of Vhost \""
219 + environment
.getVar("DOCUMENT_ROOT") + "\"";
220 logger
.logWarning(error
);
221 throw SoftException(error
, __FILE__
, __LINE__
);
223 if (config
.getCheckVHostDocroot()
224 && scriptFile
.getPath().find(environment
.getVar("DOCUMENT_ROOT"))
227 std::string error
= "File \"" + scriptFile
.getPath()
228 + "\" is not in document root of Vhost \""
229 + environment
.getVar("DOCUMENT_ROOT") + "\"";
230 logger
.logWarning(error
);
231 throw SoftException(error
, __FILE__
, __LINE__
);
234 // Check script permissions
235 // Directories will be checked later
236 if (!realScriptFile
.hasUserReadBit()) {
237 std::string error
= "File \"" + realScriptFile
.getPath()
239 logger
.logWarning(error
);
240 throw SoftException(error
, __FILE__
, __LINE__
);
244 if (!config
.getAllowFileGroupWriteable()
245 && realScriptFile
.hasGroupWriteBit()) {
246 std::string error
= "File \"" + realScriptFile
.getPath()
247 + "\" is writeable by group";
248 logger
.logWarning(error
);
249 throw SoftException(error
, __FILE__
, __LINE__
);
252 if (!config
.getAllowFileOthersWriteable()
253 && realScriptFile
.hasOthersWriteBit()) {
254 std::string error
= "File \"" + realScriptFile
.getPath()
255 + "\" is writeable by others";
256 logger
.logWarning(error
);
257 throw SoftException(error
, __FILE__
, __LINE__
);
260 // Check UID/GID of symlink is matching target
261 if (scriptFile
.getUser() != realScriptFile
.getUser()
262 || scriptFile
.getGroup() != realScriptFile
.getGroup()) {
263 std::string error
= "UID or GID of symlink \"" + scriptFile
.getPath()
264 + "\" is not matching its target";
265 logger
.logWarning(error
);
266 throw SoftException(error
, __FILE__
, __LINE__
);
270 void suPHP::Application::checkScriptFileStage2(
271 const std::string
& scriptFilename
,
272 const Configuration
& config
,
273 const Environment
& environment
,
274 const UserInfo
& targetUser
,
275 const GroupInfo
& targetGroup
) const
276 throw (SystemException
, SoftException
) {
277 Logger
& logger
= API_Helper::getSystemAPI().getSystemLogger();
278 File scriptFile
= File(scriptFilename
);
279 PathMatcher pathMatcher
= PathMatcher(targetUser
, targetGroup
);
281 // Get full path to script file
282 File realScriptFile
= File(scriptFile
.getRealPath());
284 // Check wheter script is in one of the defined docroots
285 bool file_in_docroot
= false;
286 const std::vector
<std::string
> docroots
= config
.getDocroots();
287 for (std::vector
<std::string
>::const_iterator i
= docroots
.begin(); i
!= docroots
.end(); i
++) {
288 std::string docroot
= *i
;
289 if (pathMatcher
.matches(docroot
, realScriptFile
.getPath())) {
290 file_in_docroot
= true;
294 if (!file_in_docroot
) {
295 std::string error
= "Script \"" + scriptFile
.getPath()
296 + "\" resolving to \"" + realScriptFile
.getPath()
297 + "\" not within configured docroot";
298 logger
.logWarning(error
);
299 throw SoftException(error
, __FILE__
, __LINE__
);
301 file_in_docroot
= false;
302 for (std::vector
<std::string
>::const_iterator i
= docroots
.begin(); i
!= docroots
.end(); i
++) {
303 std::string docroot
= *i
;
304 if (pathMatcher
.matches(docroot
, scriptFile
.getPath())) {
305 file_in_docroot
= true;
309 if (!file_in_docroot
) {
310 std::string error
= "Script \"" + scriptFile
.getPath()
311 + "\" not within configured docroot";
312 logger
.logWarning(error
);
313 throw SoftException(error
, __FILE__
, __LINE__
);
316 // Check directory ownership and permissions
317 checkParentDirectories(realScriptFile
, targetUser
, config
);
318 checkParentDirectories(scriptFile
, targetUser
, config
);
321 void suPHP::Application::checkProcessPermissions(
322 const std::string
& scriptFilename
,
323 const Configuration
& config
,
324 const Environment
& environment
,
325 UserInfo
& targetUser
,
326 GroupInfo
& targetGroup
) const
327 throw (SystemException
, SoftException
, SecurityException
) {
329 File scriptFile
= File(scriptFilename
);
330 File realScriptFile
= File(scriptFile
.getRealPath());
331 API
& api
= API_Helper::getSystemAPI();
332 Logger
& logger
= api
.getSystemLogger();
334 // Make sure that exactly one mode is set
336 #if !defined(OPT_USERGROUP_OWNER) && !defined(OPT_USERGROUP_FORCE) && !defined(OPT_USERGROUP_PARANOID)
337 #error "No uid/gid change model specified"
339 #if (defined(OPT_USERGROUP_OWNER) && defined(OPT_USERGROUP_FORCE)) || (defined(OPT_USERGROUP_FORCE) && defined(OPT_USERGROUP_PARANOID)) || (defined(OPT_USERGROUP_OWNER) && defined(OPT_USERGROUP_PARANOID))
340 #error "More than one uid/gid change model specified"
343 // Common code (for all security modes)
345 // Check UID/GID of script
346 if (scriptFile
.getUser().getUid() < config
.getMinUid()) {
347 std::string error
= "UID of script \"" + scriptFilename
348 + "\" is smaller than min_uid";
349 logger
.logWarning(error
);
350 throw SoftException(error
, __FILE__
, __LINE__
);
352 if (scriptFile
.getGroup().getGid() < config
.getMinGid()) {
353 std::string error
= "GID of script \"" + scriptFilename
354 + "\" is smaller than min_gid";
355 logger
.logWarning(error
);
356 throw SoftException(error
, __FILE__
, __LINE__
);
359 // Paranoid and force mode
361 #if (defined(OPT_USERGROUP_PARANOID) || defined(OPT_USERGROUP_FORCE))
362 std::string targetUsername
, targetGroupname
;
364 targetUsername
= environment
.getVar("SUPHP_USER");
365 targetGroupname
= environment
.getVar("SUPHP_GROUP");
366 } catch (KeyNotFoundException
& e
) {
367 throw SecurityException(
368 "Environment variable SUPHP_USER or SUPHP_GROUP not set",
372 if (targetUsername
[0] == '#' && targetUsername
.find_first_not_of(
373 "0123456789", 1) == std::string::npos
) {
374 targetUser
= api
.getUserInfo(Util::strToInt(targetUsername
.substr(1)));
376 targetUser
= api
.getUserInfo(targetUsername
);
379 if (targetGroupname
[0] == '#' && targetGroupname
.find_first_not_of(
380 "0123456789", 1) == std::string::npos
) {
381 targetGroup
= api
.getGroupInfo(
382 Util::strToInt(targetGroupname
.substr(1)));
384 targetGroup
= api
.getGroupInfo(targetGroupname
);
386 #endif // OPT_USERGROUP_PARANOID || OPT_USERGROUP_FORCE
390 #ifdef OPT_USERGROUP_OWNER
391 targetUser
= scriptFile
.getUser();
392 targetGroup
= scriptFile
.getGroup();
393 #endif // OPT_USERGROUP_OWNER
395 // Paranoid mode only
397 #ifdef OPT_USERGROUP_PARANOID
398 if (targetUser
!= scriptFile
.getUser()) {
399 std::string error
="Mismatch between target UID ("
400 + Util::intToStr(targetUser
.getUid()) + ") and UID ("
401 + Util::intToStr(scriptFile
.getUser().getUid()) + ") of file \""
402 + scriptFile
.getPath() + "\"";
403 logger
.logWarning(error
);
404 throw SoftException(error
, __FILE__
, __LINE__
);
407 if (targetGroup
!= scriptFile
.getGroup()) {
408 std::string error
="Mismatch between target GID ("
409 + Util::intToStr(targetGroup
.getGid()) + ") and GID ("
410 + Util::intToStr(scriptFile
.getGroup().getGid()) + ") of file \""
411 + scriptFile
.getPath() + "\"";
412 logger
.logWarning(error
);
413 throw SoftException(error
, __FILE__
, __LINE__
);
415 #endif // OPT_USERGROUP_PARANOID
418 void suPHP::Application::changeProcessPermissions(
419 const Configuration
& config
,
420 const UserInfo
& targetUser
,
421 const GroupInfo
& targetGroup
) const
422 throw (SystemException
, SoftException
, SecurityException
) {
423 API
& api
= API_Helper::getSystemAPI();
425 // Set new group first, because we still need super-user privileges
427 api
.setProcessGroup(targetGroup
);
430 api
.setProcessUser(targetUser
);
432 api
.setUmask(config
.getUmask());
436 Environment
suPHP::Application::prepareEnvironment(
437 const Environment
& sourceEnv
, const Configuration
& config
, TargetMode mode
)
438 throw (KeyNotFoundException
) {
439 // Create environment for new process from old environment
440 Environment env
= sourceEnv
;
442 // Delete unwanted environment variables
443 if (env
.hasVar("LD_PRELOAD"))
444 env
.deleteVar("LD_PRELOAD");
445 if (env
.hasVar("LD_LIBRARY_PATH"))
446 env
.deleteVar("LD_LIBRARY_PATH");
447 if (env
.hasVar("SUPHP_USER"))
448 env
.deleteVar("SUPHP_USER");
449 if (env
.hasVar("SUPHP_GROUP"))
450 env
.deleteVar("SUPHP_GROUP");
451 if (env
.hasVar("SUPHP_HANDLER"))
452 env
.deleteVar("SUPHP_HANDLER");
453 if (env
.hasVar("SUPHP_AUTH_USER"))
454 env
.deleteVar("SUPHP_AUTH_USER");
455 if (env
.hasVar("SUPHP_AUTH_PW"))
456 env
.deleteVar("SUPHP_AUTH_PW");
457 if (env
.hasVar("SUPHP_PHP_CONFIG"))
458 env
.deleteVar("SUPHP_PHP_CONFIG");
461 env
.putVar("PATH", config
.getEnvPath());
463 // If we are in PHP mode, set PHP specific variables
464 if (mode
== TARGETMODE_PHP
) {
465 if (sourceEnv
.hasVar("SUPHP_PHP_CONFIG"))
466 env
.putVar("PHPRC", sourceEnv
.getVar("SUPHP_PHP_CONFIG"));
467 if (sourceEnv
.hasVar("SUPHP_AUTH_USER")
468 && sourceEnv
.hasVar("SUPHP_AUTH_PW")) {
469 env
.putVar("PHP_AUTH_USER", sourceEnv
.getVar("SUPHP_AUTH_USER"));
470 env
.putVar("PHP_AUTH_PW", sourceEnv
.getVar("SUPHP_AUTH_PW"));
473 // PHP may need this, when compiled with security features
474 if (!env
.hasVar("REDIRECT_STATUS")) {
475 env
.putVar("REDIRECT_STATUS", "200");
483 std::string
suPHP::Application::getInterpreter(
484 const Environment
& env
, const Configuration
& config
)
485 throw (SecurityException
) {
486 if (!env
.hasVar("SUPHP_HANDLER"))
487 throw SecurityException("Environment variable SUPHP_HANDLER not set",
489 std::string handler
= env
.getVar("SUPHP_HANDLER");
491 std::string interpreter
= "";
493 interpreter
= config
.getInterpreter(handler
);
494 } catch (KeyNotFoundException
& e
) {
495 throw SecurityException ("Handler not found in configuration", e
,
503 TargetMode
suPHP::Application::getTargetMode(const std::string
& interpreter
)
504 throw (SecurityException
) {
505 if (interpreter
.substr(0, 4) == "php:")
506 return TARGETMODE_PHP
;
507 else if (interpreter
== "execute:!self")
508 return TARGETMODE_SELFEXECUTE
;
510 throw SecurityException("Unknown Interpreter: " + interpreter
,
515 void suPHP::Application::executeScript(const std::string
& scriptFilename
,
516 const std::string
& interpreter
,
518 const Environment
& env
,
519 const Configuration
& config
) const
520 throw (SoftException
) {
522 // Change working directory to script path
523 API_Helper::getSystemAPI().setCwd(
524 File(scriptFilename
).getParentDirectory().getPath());
525 if (mode
== TARGETMODE_PHP
) {
526 std::string interpreterPath
= interpreter
.substr(4);
528 cline
.putArgument(interpreterPath
);
529 API_Helper::getSystemAPI().execute(interpreterPath
, cline
, env
);
530 } else if (mode
== TARGETMODE_SELFEXECUTE
) {
532 cline
.putArgument(scriptFilename
);
533 API_Helper::getSystemAPI().execute(scriptFilename
, cline
, env
);
535 } catch (SystemException
& e
) {
536 throw SoftException("Could not execute script \"" + scriptFilename
537 + "\"", e
, __FILE__
, __LINE__
);
542 void suPHP::Application::checkParentDirectories(const File
& file
,
543 const UserInfo
& owner
,
544 const Configuration
& config
) const throw (SoftException
) {
545 File directory
= file
;
546 Logger
& logger
= API_Helper::getSystemAPI().getSystemLogger();
548 directory
= directory
.getParentDirectory();
550 UserInfo directoryOwner
= directory
.getUser();
551 if (directoryOwner
!= owner
&& !directoryOwner
.isSuperUser()) {
552 std::string error
= "Directory " + directory
.getPath()
553 + " is not owned by " + owner
.getUsername();
554 logger
.logWarning(error
);
555 throw SoftException(error
, __FILE__
, __LINE__
);
558 if (!directory
.isSymlink()
559 && !config
.getAllowDirectoryGroupWriteable()
560 && directory
.hasGroupWriteBit()) {
561 std::string error
= "Directory \"" + directory
.getPath()
562 + "\" is writeable by group";
563 logger
.logWarning(error
);
564 throw SoftException(error
, __FILE__
, __LINE__
);
567 if (!directory
.isSymlink()
568 && !config
.getAllowDirectoryOthersWriteable()
569 && directory
.hasOthersWriteBit()) {
570 std::string error
= "Directory \"" + directory
.getPath()
571 + "\" is writeable by others";
572 logger
.logWarning(error
);
573 throw SoftException(error
, __FILE__
, __LINE__
);
575 } while (directory
.getPath() != "/");
579 int main(int argc
, char **argv
) {
581 API
& api
= API_Helper::getSystemAPI();
585 for (int i
=0; i
<argc
; i
++) {
586 cmdline
.putArgument(argv
[i
]);
588 env
= api
.getProcessEnvironment();
589 return app
.run(cmdline
, env
);
590 } catch (Exception
& e
) {