72e67311e1c32dce25bda0c9ba16a44a88507f8a
[manu/suphp.git] / src / Application.cpp
1 /*
2     suPHP - (c)2002-2005 Sebastian Marsching <sebastian@marsching.com>
3
4     This file is part of suPHP.
5
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.
10
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.
15
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
19 */
20
21 #include <iostream>
22
23 #include "config.h"
24
25 #include "CommandLine.hpp"
26 #include "Environment.hpp"
27 #include "Exception.hpp"
28 #include "File.hpp"
29 #include "Configuration.hpp"
30 #include "API.hpp"
31 #include "API_Helper.hpp"
32 #include "Logger.hpp"
33 #include "UserInfo.hpp"
34 #include "GroupInfo.hpp"
35 #include "Util.hpp"
36
37 #include "Application.hpp"
38
39 using namespace suPHP;
40
41
42 suPHP::Application::Application() {
43     /* do nothing */
44 }
45
46
47 int suPHP::Application::run(CommandLine& cmdline, Environment& env) {
48     Configuration config;
49     API& api = API_Helper::getSystemAPI();
50     Logger& logger = api.getSystemLogger();
51     
52 #ifdef OPT_CONFIGFILE
53     File cfgFile = File(OPT_CONFIGFILE);
54 #else
55     File cfgFile = File("/etc/suphp.conf");
56 #endif
57
58     std::string interpreter;
59     TargetMode targetMode;
60     Environment newEnv;
61
62     // Begin try block - soft exception cannot really be handled before
63     // initialization
64     try {
65         std::string scriptFilename;
66         
67         // If caller is super-user, print info message and exit
68         if (api.getRealProcessUser().isSuperUser()) {
69             this->printAboutMessage();
70             return 0;
71         }
72         config.readFromFile(cfgFile);
73         
74         // Check permissions (real uid, effective uid)
75         this->checkProcessPermissions(config);
76         
77         // Initialize logger
78         // not done before, because we need super-user privileges for
79         // logging anyway
80         logger.init(config);
81
82         try {
83             scriptFilename = env.getVar("SCRIPT_FILENAME");
84         } catch (KeyNotFoundException& e) {
85             logger.logError("Environment variable SCRIPT_FILENAME not set");
86             this->printAboutMessage();
87             return 1;
88         }
89
90         this->checkScriptFile(scriptFilename, config, env);
91
92         // Root privileges are needed for chroot()
93         // so do this before changing process permissions
94         if (config.getChrootPath().length() > 0) {
95             api.chroot(config.getChrootPath());
96         }
97
98         this->changeProcessPermissions(scriptFilename, config, env);
99
100         interpreter = this->getInterpreter(env, config);
101         targetMode = this->getTargetMode(interpreter);
102         
103         // Prepare environment for new process
104         newEnv = this->prepareEnvironment(env, config, targetMode);
105         
106         // Set PATH_TRANSLATED to SCRIPT_FILENAME, otherwise
107         // the PHP interpreter will not be able to find the script
108         if (targetMode == TARGETMODE_PHP && newEnv.hasVar("PATH_TRANSLATED")) {
109             newEnv.setVar("PATH_TRANSLATED", scriptFilename);
110         }
111         
112         // Log attempt to execute script
113         logger.logInfo("Executing \"" + scriptFilename + "\" as UID "
114                        + Util::intToStr(api.getEffectiveProcessUser().getUid())
115                        + ", GID " 
116                        + Util::intToStr(
117                            api.getEffectiveProcessGroup().getGid()));
118
119         this->executeScript(scriptFilename, interpreter, targetMode, newEnv, 
120                             config);
121         
122         // Function should never return
123         // So, if we get here, return with error code
124         return 1;
125     } catch (SoftException& e) {
126         if (!config.getErrorsToBrowser()) {
127             std::cerr << e;
128             return 2;
129         }
130         std::cout << "Content-Type: text/html\n"
131                   << "Status: 500\n"
132                   << "\n"
133                   << "<html>\n"
134                   << " <head>\n"
135                   << "  <title>500 Internal Server Error</title>\n"
136                   << " </head>\n"
137                   << " <body>\n"
138                   << "  <h1>Internal Server Error</h1>\n"
139                   << "  <p>" << e.getMessage() << "</p>\n"
140                   << "  <hr/>"
141                   << "  <address>suPHP " << PACKAGE_VERSION << "</address>\n"
142                   << " </body>\n"
143                   << "</html>\n";
144     }
145 }
146
147
148 void suPHP::Application::printAboutMessage() {
149     std::cerr << "suPHP version " << PACKAGE_VERSION << "\n";
150     std::cerr << "(c) 2002-2005 Sebastian Marsching\n";
151     std::cerr << std::endl;
152     std::cerr << "suPHP has to be called by mod_suphp to work." << std::endl;
153 }
154
155
156 void suPHP::Application::checkProcessPermissions(Configuration& config) 
157     throw (SecurityException, LookupException) {
158     API& api = API_Helper::getSystemAPI();
159     if (api.getRealProcessUser() !=
160         api.getUserInfo(config.getWebserverUser())) {
161         throw SecurityException("Calling user is not webserver user!",
162                                 __FILE__, __LINE__);
163     }
164     
165     if (!api.getEffectiveProcessUser().isSuperUser()) {
166         throw SecurityException(
167             "Do not have root privileges. Executable not set-uid root?",
168             __FILE__, __LINE__);
169     }
170 }
171
172
173 void suPHP::Application::checkScriptFile(
174     const std::string& scriptFilename, 
175     const Configuration& config,
176     const Environment& environment) const
177     throw (SystemException, SoftException) {
178     Logger& logger = API_Helper::getSystemAPI().getSystemLogger();
179     File scriptFile = File(scriptFilename);
180     File realScriptFile = File(scriptFile.getRealPath());
181
182     // Check wheter file exists
183     if (!scriptFile.exists()) {
184         std::string error = "File " + scriptFile.getPath() + " does not exist";
185         logger.logWarning(error);
186         throw SoftException(error, __FILE__, __LINE__);
187     }
188     if (!realScriptFile.exists()) {
189         std::string error = "File " + realScriptFile.getPath() 
190             + " referenced by symlink " +scriptFile.getPath() 
191             + " does not exist";
192         logger.logWarning(error);
193         throw SoftException(error, __FILE__, __LINE__);
194     }
195     
196     // Check wheter script is in docroot
197     if (realScriptFile.getPath().find(config.getDocroot()) != 0) {
198         std::string error = "Script \"" + scriptFile.getPath() 
199             + "\" resolving to \"" + realScriptFile.getPath() 
200             + "\" not within configured docroot";
201         logger.logWarning(error);
202         throw SoftException(error, __FILE__, __LINE__);
203     }
204
205     // If enabled, check whether script is in the vhost's docroot
206     if (!environment.hasVar("DOCUMENT_ROOT"))
207         throw SoftException("Environment variable DOCUMENT_ROOT not set",
208                                 __FILE__, __LINE__);
209     if (config.getCheckVHostDocroot()
210         && realScriptFile.getPath().find(environment.getVar("DOCUMENT_ROOT")) 
211         != 0) {
212         
213         std::string error = "File \"" + realScriptFile.getPath()
214             + "\" is not in document root of Vhost \""
215             + environment.getVar("DOCUMENT_ROOT") + "\"";
216         logger.logWarning(error);
217         throw SoftException(error, __FILE__, __LINE__);
218     }
219     if (config.getCheckVHostDocroot()
220         && scriptFile.getPath().find(environment.getVar("DOCUMENT_ROOT")) 
221         != 0) {
222         
223         std::string error = "File \"" + scriptFile.getPath()
224             + "\" is not in document root of Vhost \""
225             + environment.getVar("DOCUMENT_ROOT") + "\"";
226         logger.logWarning(error);
227         throw SoftException(error, __FILE__, __LINE__);
228     }
229
230     // Check script permissions
231     // Directories will be checked later
232     if (!realScriptFile.hasUserReadBit()) {
233         std::string error = "File \"" + realScriptFile.getPath()
234             + "\" not readable";
235         logger.logWarning(error);
236         throw SoftException(error, __FILE__, __LINE__);
237         
238     }
239
240     if (!config.getAllowFileGroupWriteable()
241         && realScriptFile.hasGroupWriteBit()) {
242         std::string error = "File \"" + realScriptFile.getPath()
243             + "\" is writeable by group";
244         logger.logWarning(error);
245         throw SoftException(error, __FILE__, __LINE__);
246     }
247     
248     if (!config.getAllowFileOthersWriteable()
249         && realScriptFile.hasOthersWriteBit()) {
250         std::string error = "File \"" + realScriptFile.getPath()
251             + "\" is writeable by others";
252         logger.logWarning(error);
253         throw SoftException(error, __FILE__, __LINE__);
254     }
255     
256     // Check UID/GID of symlink is matching target
257     if (scriptFile.getUser() != realScriptFile.getUser()
258         || scriptFile.getGroup() != realScriptFile.getGroup()) {
259         std::string error = "UID or GID of symlink \"" + scriptFile.getPath() 
260             + "\" is not matching its target";
261         logger.logWarning(error);
262         throw SoftException(error, __FILE__, __LINE__);
263     }
264 }
265
266
267 void suPHP::Application::changeProcessPermissions(
268     const std::string& scriptFilename, 
269     const Configuration& config,
270     const Environment& environment) const
271     throw (SystemException, SoftException, SecurityException) {
272     UserInfo targetUser;
273     GroupInfo targetGroup;
274
275     File scriptFile = File(scriptFilename);
276     File realScriptFile = File(scriptFile.getRealPath());
277     API& api = API_Helper::getSystemAPI();
278     Logger& logger = api.getSystemLogger();
279
280     // Make sure that exactly one mode is set
281
282 #if !defined(OPT_USERGROUP_OWNER) && !defined(OPT_USERGROUP_FORCE) && !defined(OPT_USERGROUP_PARANOID)
283 #error "No uid/gid change model specified"
284 #endif
285 #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))
286 #error "More than one uid/gid change model specified"
287 #endif
288
289     // Common code (for all security modes)
290
291     // Check UID/GID of script
292     if (scriptFile.getUser().getUid() < config.getMinUid()) {
293         std::string error = "UID of script \"" + scriptFilename
294             + "\" is smaller than min_uid";
295         logger.logWarning(error);
296         throw SoftException(error, __FILE__, __LINE__);
297     }
298     if (scriptFile.getGroup().getGid() < config.getMinGid()) {
299         std::string error = "GID of script \"" + scriptFilename
300             + "\" is smaller than min_gid";
301         logger.logWarning(error);
302         throw SoftException(error, __FILE__, __LINE__);
303     }
304     
305     // Paranoid and force mode
306
307 #if (defined(OPT_USERGROUP_PARANOID) || defined(OPT_USERGROUP_FORCE))
308     std::string targetUsername, targetGroupname;
309     try {
310         targetUsername = environment.getVar("SUPHP_USER");
311         targetGroupname = environment.getVar("SUPHP_GROUP");
312     } catch (KeyNotFoundException& e) {
313         throw SecurityException(
314             "Environment variable SUPHP_USER or SUPHP_GROUP not set", 
315             __FILE__, __LINE__);
316     }
317     
318     if (targetUsername[0] == '#' && targetUsername.find_first_not_of(
319             "0123456789", 1) == std::string::npos) {
320         targetUser = api.getUserInfo(Util::strToInt(targetUsername.substr(1)));
321     } else {
322         targetUser = api.getUserInfo(targetUsername);
323     }
324
325     if (targetGroupname[0] == '#' && targetGroupname.find_first_not_of(
326             "0123456789", 1) == std::string::npos) {
327         targetGroup = api.getGroupInfo(
328             Util::strToInt(targetGroupname.substr(1)));
329     } else {
330         targetGroup = api.getGroupInfo(targetGroupname);
331     }
332 #endif // OPT_USERGROUP_PARANOID || OPT_USERGROUP_FORCE
333
334     // Owner mode only
335
336 #ifdef OPT_USERGROUP_OWNER
337     targetUser = scriptFile.getUser();
338     targetGroup = scriptFile.getGroup();
339 #endif // OPT_USERGROUP_OWNER
340     
341     // Paranoid mode only
342
343 #ifdef OPT_USERGROUP_PARANOID
344     if (targetUser != scriptFile.getUser()) {
345         std::string error ="Mismatch between target UID ("
346             + Util::intToStr(targetUser.getUid()) + ") and UID (" 
347             + Util::intToStr(scriptFile.getUser().getUid()) + ") of file \"" 
348             + scriptFile.getPath() + "\"";
349         logger.logWarning(error);
350         throw SoftException(error, __FILE__, __LINE__);
351     }
352
353     if (targetGroup != scriptFile.getGroup()) {
354         std::string error ="Mismatch between target GID ("
355             + Util::intToStr(targetGroup.getGid()) + ") and GID (" 
356             + Util::intToStr(scriptFile.getGroup().getGid()) + ") of file \"" 
357             + scriptFile.getPath() + "\"";
358         logger.logWarning(error);
359         throw SoftException(error, __FILE__, __LINE__);
360     }
361 #endif // OPT_USERGROUP_PARANOID    
362     
363     // Check directory ownership and permissions
364     checkParentDirectories(realScriptFile, targetUser, config);
365     checkParentDirectories(scriptFile, targetUser, config);
366     
367     // Common code used for all modes
368
369     // Set new group first, because we still need super-user privileges
370     // for this
371     api.setProcessGroup(targetGroup);
372     
373     // Then set new user
374     api.setProcessUser(targetUser);
375
376     api.setUmask(config.getUmask());
377 }
378
379
380 Environment suPHP::Application::prepareEnvironment(
381     const Environment& sourceEnv, const Configuration& config, TargetMode mode)
382     throw (KeyNotFoundException) {
383     // Create environment for new process from old environment
384     Environment env = sourceEnv;
385     
386     // Delete unwanted environment variables
387     if (env.hasVar("LD_PRELOAD"))
388         env.deleteVar("LD_PRELOAD");
389     if (env.hasVar("LD_LIBRARY_PATH"))
390         env.deleteVar("LD_LIBRARY_PATH");
391     if (env.hasVar("SUPHP_USER"))
392         env.deleteVar("SUPHP_USER");
393     if (env.hasVar("SUPHP_GROUP"))
394         env.deleteVar("SUPHP_GROUP");
395     if (env.hasVar("SUPHP_HANDLER"))
396         env.deleteVar("SUPHP_HANDLER");
397     if (env.hasVar("SUPHP_AUTH_USER"))
398         env.deleteVar("SUPHP_AUTH_USER");
399     if (env.hasVar("SUPHP_AUTH_PW"))
400         env.deleteVar("SUPHP_AUTH_PW");
401     if (env.hasVar("SUPHP_PHP_CONFIG"))
402         env.deleteVar("SUPHP_PHP_CONFIG");
403     
404     // Reset PATH
405     env.putVar("PATH", config.getEnvPath());
406
407     // If we are in PHP mode, set PHP specific variables
408     if (mode == TARGETMODE_PHP) {
409         if (sourceEnv.hasVar("SUPHP_PHP_CONFIG"))
410             env.putVar("PHPRC", sourceEnv.getVar("SUPHP_PHP_CONFIG"));
411         if (sourceEnv.hasVar("SUPHP_AUTH_USER")
412             && sourceEnv.hasVar("SUPHP_AUTH_PW")) {
413             env.putVar("PHP_AUTH_USER", sourceEnv.getVar("SUPHP_AUTH_USER"));
414             env.putVar("PHP_AUTH_PW", sourceEnv.getVar("SUPHP_AUTH_PW"));
415         }
416
417         // PHP may need this, when compiled with security features
418         if (!env.hasVar("REDIRECT_STATUS")) {
419             env.putVar("REDIRECT_STATUS", "200");
420         }
421     }
422     
423     return env;
424 }
425
426
427 std::string suPHP::Application::getInterpreter(
428     const Environment& env, const Configuration& config)
429     throw (SecurityException) {
430     if (!env.hasVar("SUPHP_HANDLER"))
431         throw SecurityException("Environment variable SUPHP_HANDLER not set",
432                                 __FILE__, __LINE__);
433     std::string handler = env.getVar("SUPHP_HANDLER");
434     
435     std::string interpreter = "";
436     try {
437         interpreter = config.getInterpreter(handler);
438     } catch (KeyNotFoundException& e) {
439         throw SecurityException ("Handler not found in configuration", e,
440                                  __FILE__, __LINE__);
441     }
442     
443     return interpreter;
444 }
445
446
447 TargetMode suPHP::Application::getTargetMode(const std::string& interpreter)
448     throw (SecurityException) {
449     if (interpreter.substr(0, 4) == "php:")
450         return TARGETMODE_PHP;
451     else if (interpreter == "execute:!self")
452         return TARGETMODE_SELFEXECUTE;
453     else
454         throw SecurityException("Unknown Interpreter: " + interpreter,
455                                 __FILE__, __LINE__);
456 }
457
458
459 void suPHP::Application::executeScript(const std::string& scriptFilename,
460                                        const std::string& interpreter,
461                                        TargetMode mode,
462                                        const Environment& env,
463                                        const Configuration& config) const
464     throw (SoftException) {
465     try {
466         // Change working directory to script path
467         API_Helper::getSystemAPI().setCwd(
468             File(scriptFilename).getParentDirectory().getPath());
469         if (mode == TARGETMODE_PHP) {
470             std::string interpreterPath = interpreter.substr(4);
471             CommandLine cline;
472             cline.putArgument(interpreterPath);
473             API_Helper::getSystemAPI().execute(interpreterPath, cline, env);
474         } else if (mode == TARGETMODE_SELFEXECUTE) {
475             CommandLine cline;
476             cline.putArgument(scriptFilename);
477             API_Helper::getSystemAPI().execute(scriptFilename, cline, env);
478         }
479     } catch (SystemException& e) {
480         throw SoftException("Could not execute script \"" + scriptFilename
481                                 + "\"", e, __FILE__, __LINE__);
482     }
483 }
484
485
486 void suPHP::Application::checkParentDirectories(const File& file,
487                                                const UserInfo& owner,
488                                                const Configuration& config) const throw (SoftException) {
489     File directory = file;
490     Logger& logger = API_Helper::getSystemAPI().getSystemLogger();
491     do {
492         directory = directory.getParentDirectory();
493         
494         UserInfo directoryOwner = directory.getUser();
495         if (directoryOwner != owner && !directoryOwner.isSuperUser()) {
496             std::string error = "Directory " + directory.getPath()
497                 + " is not owned by " + owner.getUsername();
498             logger.logWarning(error);
499             throw SoftException(error, __FILE__, __LINE__);
500         }
501         
502         if (!directory.isSymlink()
503             && !config.getAllowDirectoryGroupWriteable() 
504             && directory.hasGroupWriteBit()) {
505             std::string error = "Directory \"" + directory.getPath()
506                 + "\" is writeable by group";
507             logger.logWarning(error);
508             throw SoftException(error, __FILE__, __LINE__);
509         }
510         
511         if (!directory.isSymlink()
512             && !config.getAllowDirectoryOthersWriteable()
513             && directory.hasOthersWriteBit()) {
514             std::string error = "Directory \"" + directory.getPath()
515                 + "\" is writeable by others";
516             logger.logWarning(error);
517             throw SoftException(error, __FILE__, __LINE__);
518         }
519     } while (directory.getPath() != "/");
520 }
521
522
523 int main(int argc, char **argv) {
524     try {
525         API& api = API_Helper::getSystemAPI();
526         CommandLine cmdline;
527         Environment env;
528         Application app;
529         for (int i=0; i<argc; i++) {
530             cmdline.putArgument(argv[i]);
531         }
532         env = api.getProcessEnvironment();
533         return app.run(cmdline, env);
534     } catch (Exception& e) {
535         std::cerr << e;
536         return 1;
537     }
538 }