/* suPHP - (c)2002-2008 Sebastian Marsching This file is part of suPHP. suPHP is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. suPHP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with suPHP; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include "config.h" #include "CommandLine.hpp" #include "Environment.hpp" #include "Exception.hpp" #include "File.hpp" #include "Configuration.hpp" #include "API.hpp" #include "API_Helper.hpp" #include "Logger.hpp" #include "UserInfo.hpp" #include "GroupInfo.hpp" #include "Util.hpp" #include "PathMatcher.hpp" #include "Application.hpp" using namespace suPHP; suPHP::Application::Application() { /* do nothing */ } int suPHP::Application::run(CommandLine& cmdline, Environment& env) { Configuration config; API& api = API_Helper::getSystemAPI(); Logger& logger = api.getSystemLogger(); #ifdef OPT_CONFIGFILE File cfgFile = File(OPT_CONFIGFILE); #else File cfgFile = File("/etc/suphp.conf"); #endif std::string interpreter; TargetMode targetMode; Environment newEnv; // Begin try block - soft exception cannot really be handled before // initialization try { std::string scriptFilename; UserInfo targetUser; GroupInfo targetGroup; // If caller is super-user, print info message and exit if (api.getRealProcessUser().isSuperUser()) { this->printAboutMessage(); return 0; } config.readFromFile(cfgFile); // Check permissions (real uid, effective uid) this->checkProcessPermissions(config); // Initialize logger // not done before, because we need super-user privileges for // logging anyway logger.init(config); try { scriptFilename = env.getVar("SCRIPT_FILENAME"); } catch (KeyNotFoundException& e) { logger.logError("Environment variable SCRIPT_FILENAME not set"); this->printAboutMessage(); return 1; } // Do checks that do not need target user info this->checkScriptFileStage1(scriptFilename, config, env); // Find out target user this->checkProcessPermissions(scriptFilename, config, env, targetUser, targetGroup); // Now do checks that might require user info this->checkScriptFileStage2(scriptFilename, config, env, targetUser, targetGroup); // Root privileges are needed for chroot() // so do this before changing process permissions if (config.getChrootPath().length() > 0) { PathMatcher pathMatcher = PathMatcher(targetUser, targetGroup); std::string chrootPath = pathMatcher.resolveVariables(config.getChrootPath()); api.chroot(chrootPath); } this->changeProcessPermissions(config, targetUser, targetGroup); interpreter = this->getInterpreter(env, config); targetMode = this->getTargetMode(interpreter); // Prepare environment for new process newEnv = this->prepareEnvironment(env, config, targetMode); // Set PATH_TRANSLATED to SCRIPT_FILENAME, otherwise // the PHP interpreter will not be able to find the script if (targetMode == TARGETMODE_PHP && newEnv.hasVar("PATH_TRANSLATED")) { newEnv.setVar("PATH_TRANSLATED", scriptFilename); } // Log attempt to execute script logger.logInfo("Executing \"" + scriptFilename + "\" as UID " + Util::intToStr(api.getEffectiveProcessUser().getUid()) + ", GID " + Util::intToStr( api.getEffectiveProcessGroup().getGid())); this->executeScript(scriptFilename, interpreter, targetMode, newEnv, config); // Function should never return // So, if we get here, return with error code return 1; } catch (SoftException& e) { if (!config.getErrorsToBrowser()) { std::cerr << e; return 2; } std::cout << "Content-Type: text/html\n" << "Status: 500\n" << "\n" << "\n" << " \n" << " 500 Internal Server Error\n" << " \n" << " \n" << "

Internal Server Error

\n" << "

" << e.getMessage() << "

\n" << "
" << "
suPHP " << PACKAGE_VERSION << "
\n" << " \n" << "\n"; } } void suPHP::Application::printAboutMessage() { std::cerr << "suPHP version " << PACKAGE_VERSION << "\n"; std::cerr << "(c) 2002-2007 Sebastian Marsching\n"; std::cerr << std::endl; std::cerr << "suPHP has to be called by mod_suphp to work." << std::endl; } void suPHP::Application::checkProcessPermissions(Configuration& config) throw (SecurityException, LookupException) { API& api = API_Helper::getSystemAPI(); if (api.getRealProcessUser() != api.getUserInfo(config.getWebserverUser())) { throw SecurityException("Calling user is not webserver user!", __FILE__, __LINE__); } if (!api.getEffectiveProcessUser().isSuperUser()) { throw SecurityException( "Do not have root privileges. Executable not set-uid root?", __FILE__, __LINE__); } } void suPHP::Application::checkScriptFileStage1( const std::string& scriptFilename, const Configuration& config, const Environment& environment) const throw (SystemException, SoftException) { Logger& logger = API_Helper::getSystemAPI().getSystemLogger(); File scriptFile = File(scriptFilename); File realScriptFile = File(scriptFile.getRealPath()); // Check wheter file exists if (!scriptFile.exists()) { std::string error = "File " + scriptFile.getPath() + " does not exist"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } if (!realScriptFile.exists()) { std::string error = "File " + realScriptFile.getPath() + " referenced by symlink " +scriptFile.getPath() + " does not exist"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } // If enabled, check whether script is in the vhost's docroot if (!environment.hasVar("DOCUMENT_ROOT")) throw SoftException("Environment variable DOCUMENT_ROOT not set", __FILE__, __LINE__); if (config.getCheckVHostDocroot() && realScriptFile.getPath().find(environment.getVar("DOCUMENT_ROOT")) != 0) { std::string error = "File \"" + realScriptFile.getPath() + "\" is not in document root of Vhost \"" + environment.getVar("DOCUMENT_ROOT") + "\""; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } if (config.getCheckVHostDocroot() && scriptFile.getPath().find(environment.getVar("DOCUMENT_ROOT")) != 0) { std::string error = "File \"" + scriptFile.getPath() + "\" is not in document root of Vhost \"" + environment.getVar("DOCUMENT_ROOT") + "\""; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } // Check script permissions // Directories will be checked later if (!realScriptFile.hasUserReadBit()) { std::string error = "File \"" + realScriptFile.getPath() + "\" not readable"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } if (!config.getAllowFileGroupWriteable() && realScriptFile.hasGroupWriteBit()) { std::string error = "File \"" + realScriptFile.getPath() + "\" is writeable by group"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } if (!config.getAllowFileOthersWriteable() && realScriptFile.hasOthersWriteBit()) { std::string error = "File \"" + realScriptFile.getPath() + "\" is writeable by others"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } // Check UID/GID of symlink is matching target if (scriptFile.getUser() != realScriptFile.getUser() || scriptFile.getGroup() != realScriptFile.getGroup()) { std::string error = "UID or GID of symlink \"" + scriptFile.getPath() + "\" is not matching its target"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } } void suPHP::Application::checkScriptFileStage2( const std::string& scriptFilename, const Configuration& config, const Environment& environment, const UserInfo& targetUser, const GroupInfo& targetGroup) const throw (SystemException, SoftException) { Logger& logger = API_Helper::getSystemAPI().getSystemLogger(); File scriptFile = File(scriptFilename); PathMatcher pathMatcher = PathMatcher(targetUser, targetGroup); // Get full path to script file File realScriptFile = File(scriptFile.getRealPath()); // Check wheter script is in one of the defined docroots bool file_in_docroot = false; const std::vector docroots = config.getDocroots(); for (std::vector::const_iterator i = docroots.begin(); i != docroots.end(); i++) { std::string docroot = *i; if (pathMatcher.matches(docroot, realScriptFile.getPath())) { file_in_docroot = true; break; } } if (!file_in_docroot) { std::string error = "Script \"" + scriptFile.getPath() + "\" resolving to \"" + realScriptFile.getPath() + "\" not within configured docroot"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } file_in_docroot = false; for (std::vector::const_iterator i = docroots.begin(); i != docroots.end(); i++) { std::string docroot = *i; if (pathMatcher.matches(docroot, scriptFile.getPath())) { file_in_docroot = true; break; } } if (!file_in_docroot) { std::string error = "Script \"" + scriptFile.getPath() + "\" not within configured docroot"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } // Check directory ownership and permissions checkParentDirectories(realScriptFile, targetUser, config); checkParentDirectories(scriptFile, targetUser, config); } void suPHP::Application::checkProcessPermissions( const std::string& scriptFilename, const Configuration& config, const Environment& environment, UserInfo& targetUser, GroupInfo& targetGroup) const throw (SystemException, SoftException, SecurityException) { File scriptFile = File(scriptFilename); File realScriptFile = File(scriptFile.getRealPath()); API& api = API_Helper::getSystemAPI(); Logger& logger = api.getSystemLogger(); // Make sure that exactly one mode is set #if !defined(OPT_USERGROUP_OWNER) && !defined(OPT_USERGROUP_FORCE) && !defined(OPT_USERGROUP_PARANOID) #error "No uid/gid change model specified" #endif #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)) #error "More than one uid/gid change model specified" #endif // Common code (for all security modes) // Check UID/GID of script if (scriptFile.getUser().getUid() < config.getMinUid()) { std::string error = "UID of script \"" + scriptFilename + "\" is smaller than min_uid"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } if (scriptFile.getGroup().getGid() < config.getMinGid()) { std::string error = "GID of script \"" + scriptFilename + "\" is smaller than min_gid"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } // Paranoid and force mode #if (defined(OPT_USERGROUP_PARANOID) || defined(OPT_USERGROUP_FORCE)) std::string targetUsername, targetGroupname; try { targetUsername = environment.getVar("SUPHP_USER"); targetGroupname = environment.getVar("SUPHP_GROUP"); } catch (KeyNotFoundException& e) { throw SecurityException( "Environment variable SUPHP_USER or SUPHP_GROUP not set", __FILE__, __LINE__); } if (targetUsername[0] == '#' && targetUsername.find_first_not_of( "0123456789", 1) == std::string::npos) { targetUser = api.getUserInfo(Util::strToInt(targetUsername.substr(1))); } else { targetUser = api.getUserInfo(targetUsername); } if (targetGroupname[0] == '#' && targetGroupname.find_first_not_of( "0123456789", 1) == std::string::npos) { targetGroup = api.getGroupInfo( Util::strToInt(targetGroupname.substr(1))); } else { targetGroup = api.getGroupInfo(targetGroupname); } #endif // OPT_USERGROUP_PARANOID || OPT_USERGROUP_FORCE // Owner mode only #ifdef OPT_USERGROUP_OWNER targetUser = scriptFile.getUser(); targetGroup = scriptFile.getGroup(); #endif // OPT_USERGROUP_OWNER // Paranoid mode only #ifdef OPT_USERGROUP_PARANOID if (targetUser != scriptFile.getUser()) { std::string error ="Mismatch between target UID (" + Util::intToStr(targetUser.getUid()) + ") and UID (" + Util::intToStr(scriptFile.getUser().getUid()) + ") of file \"" + scriptFile.getPath() + "\""; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } if (targetGroup != scriptFile.getGroup()) { std::string error ="Mismatch between target GID (" + Util::intToStr(targetGroup.getGid()) + ") and GID (" + Util::intToStr(scriptFile.getGroup().getGid()) + ") of file \"" + scriptFile.getPath() + "\""; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } #endif // OPT_USERGROUP_PARANOID } void suPHP::Application::changeProcessPermissions( const Configuration& config, const UserInfo& targetUser, const GroupInfo& targetGroup) const throw (SystemException, SoftException, SecurityException) { API& api = API_Helper::getSystemAPI(); // Set new group first, because we still need super-user privileges // for this api.setProcessGroup(targetGroup); // Then set new user api.setProcessUser(targetUser); api.setUmask(config.getUmask()); } Environment suPHP::Application::prepareEnvironment( const Environment& sourceEnv, const Configuration& config, TargetMode mode) throw (KeyNotFoundException) { // Create environment for new process from old environment Environment env = sourceEnv; // Delete unwanted environment variables if (env.hasVar("LD_PRELOAD")) env.deleteVar("LD_PRELOAD"); if (env.hasVar("LD_LIBRARY_PATH")) env.deleteVar("LD_LIBRARY_PATH"); if (env.hasVar("SUPHP_USER")) env.deleteVar("SUPHP_USER"); if (env.hasVar("SUPHP_GROUP")) env.deleteVar("SUPHP_GROUP"); if (env.hasVar("SUPHP_HANDLER")) env.deleteVar("SUPHP_HANDLER"); if (env.hasVar("SUPHP_AUTH_USER")) env.deleteVar("SUPHP_AUTH_USER"); if (env.hasVar("SUPHP_AUTH_PW")) env.deleteVar("SUPHP_AUTH_PW"); if (env.hasVar("SUPHP_PHP_CONFIG")) env.deleteVar("SUPHP_PHP_CONFIG"); // Reset PATH env.putVar("PATH", config.getEnvPath()); // If we are in PHP mode, set PHP specific variables if (mode == TARGETMODE_PHP) { if (sourceEnv.hasVar("SUPHP_PHP_CONFIG")) env.putVar("PHPRC", sourceEnv.getVar("SUPHP_PHP_CONFIG")); if (sourceEnv.hasVar("SUPHP_AUTH_USER") && sourceEnv.hasVar("SUPHP_AUTH_PW")) { env.putVar("PHP_AUTH_USER", sourceEnv.getVar("SUPHP_AUTH_USER")); env.putVar("PHP_AUTH_PW", sourceEnv.getVar("SUPHP_AUTH_PW")); } // PHP may need this, when compiled with security features if (!env.hasVar("REDIRECT_STATUS")) { env.putVar("REDIRECT_STATUS", "200"); } } return env; } std::string suPHP::Application::getInterpreter( const Environment& env, const Configuration& config) throw (SecurityException) { if (!env.hasVar("SUPHP_HANDLER")) throw SecurityException("Environment variable SUPHP_HANDLER not set", __FILE__, __LINE__); std::string handler = env.getVar("SUPHP_HANDLER"); std::string interpreter = ""; try { interpreter = config.getInterpreter(handler); } catch (KeyNotFoundException& e) { throw SecurityException ("Handler not found in configuration", e, __FILE__, __LINE__); } return interpreter; } TargetMode suPHP::Application::getTargetMode(const std::string& interpreter) throw (SecurityException) { if (interpreter.substr(0, 4) == "php:") return TARGETMODE_PHP; else if (interpreter == "execute:!self") return TARGETMODE_SELFEXECUTE; else throw SecurityException("Unknown Interpreter: " + interpreter, __FILE__, __LINE__); } void suPHP::Application::executeScript(const std::string& scriptFilename, const std::string& interpreter, TargetMode mode, const Environment& env, const Configuration& config) const throw (SoftException) { try { // Change working directory to script path API_Helper::getSystemAPI().setCwd( File(scriptFilename).getParentDirectory().getPath()); if (mode == TARGETMODE_PHP) { std::string interpreterPath = interpreter.substr(4); CommandLine cline; cline.putArgument(interpreterPath); API_Helper::getSystemAPI().execute(interpreterPath, cline, env); } else if (mode == TARGETMODE_SELFEXECUTE) { CommandLine cline; cline.putArgument(scriptFilename); API_Helper::getSystemAPI().execute(scriptFilename, cline, env); } } catch (SystemException& e) { throw SoftException("Could not execute script \"" + scriptFilename + "\"", e, __FILE__, __LINE__); } } void suPHP::Application::checkParentDirectories(const File& file, const UserInfo& owner, const Configuration& config) const throw (SoftException) { File directory = file; Logger& logger = API_Helper::getSystemAPI().getSystemLogger(); do { directory = directory.getParentDirectory(); UserInfo directoryOwner = directory.getUser(); if (directoryOwner != owner && !directoryOwner.isSuperUser()) { std::string error = "Directory " + directory.getPath() + " is not owned by " + owner.getUsername(); logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } if (!directory.isSymlink() && !config.getAllowDirectoryGroupWriteable() && directory.hasGroupWriteBit()) { std::string error = "Directory \"" + directory.getPath() + "\" is writeable by group"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } if (!directory.isSymlink() && !config.getAllowDirectoryOthersWriteable() && directory.hasOthersWriteBit()) { std::string error = "Directory \"" + directory.getPath() + "\" is writeable by others"; logger.logWarning(error); throw SoftException(error, __FILE__, __LINE__); } } while (directory.getPath() != "/"); } int main(int argc, char **argv) { try { API& api = API_Helper::getSystemAPI(); CommandLine cmdline; Environment env; Application app; for (int i=0; i