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) {