Import upstream 0.7.1
[manu/suphp.git] / src / Application.cpp
1 /*
2     suPHP - (c)2002-2008 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 #include "PathMatcher.hpp"
37
38 #include "Application.hpp"
39
40 using namespace suPHP;
41
42
43 suPHP::Application::Application() {
44     /* do nothing */
45 }
46
47
48 int suPHP::Application::run(CommandLine& cmdline, Environment& env) {
49     Configuration config;
50     API& api = API_Helper::getSystemAPI();
51     Logger& logger = api.getSystemLogger();
52
53 #ifdef OPT_CONFIGFILE
54     File cfgFile = File(OPT_CONFIGFILE);
55 #else
56     File cfgFile = File("/etc/suphp.conf");
57 #endif
58
59     std::string interpreter;
60     TargetMode targetMode;
61     Environment newEnv;
62
63     // Begin try block - soft exception cannot really be handled before
64     // initialization
65     try {
66         std::string scriptFilename;
67         UserInfo targetUser;
68         GroupInfo targetGroup;
69
70         // If caller is super-user, print info message and exit
71         if (api.getRealProcessUser().isSuperUser()) {
72             this->printAboutMessage();
73             return 0;
74         }
75         config.readFromFile(cfgFile);
76
77         // Check permissions (real uid, effective uid)
78         this->checkProcessPermissions(config);
79
80         // Initialize logger
81         // not done before, because we need super-user privileges for
82         // logging anyway
83         logger.init(config);
84
85         try {
86             scriptFilename = env.getVar("SCRIPT_FILENAME");
87         } catch (KeyNotFoundException& e) {
88             logger.logError("Environment variable SCRIPT_FILENAME not set");
89             this->printAboutMessage();
90             return 1;
91         }
92
93
94         // Do checks that do not need target user info
95         this->checkScriptFileStage1(scriptFilename, config, env);
96
97         // Find out target user
98         this->checkProcessPermissions(scriptFilename, config, env, targetUser, targetGroup);
99
100         // Now do checks that might require user info
101         this->checkScriptFileStage2(scriptFilename, config, env, targetUser, targetGroup);
102
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);
109         }
110
111         this->changeProcessPermissions(config, targetUser, targetGroup);
112
113         interpreter = this->getInterpreter(env, config);
114         targetMode = this->getTargetMode(interpreter);
115
116         // Prepare environment for new process
117         newEnv = this->prepareEnvironment(env, config, targetMode);
118
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);
123         }
124
125         // Log attempt to execute script
126         logger.logInfo("Executing \"" + scriptFilename + "\" as UID "
127                        + Util::intToStr(api.getEffectiveProcessUser().getUid())
128                        + ", GID "
129                        + Util::intToStr(
130                            api.getEffectiveProcessGroup().getGid()));
131
132         this->executeScript(scriptFilename, interpreter, targetMode, newEnv,
133                             config);
134
135         // Function should never return
136         // So, if we get here, return with error code
137         return 1;
138     } catch (SoftException& e) {
139         if (!config.getErrorsToBrowser()) {
140             std::cerr << e;
141             return 2;
142         }
143         std::cout << "Content-Type: text/html\n"
144                   << "Status: 500\n"
145                   << "\n"
146                   << "<html>\n"
147                   << " <head>\n"
148                   << "  <title>500 Internal Server Error</title>\n"
149                   << " </head>\n"
150                   << " <body>\n"
151                   << "  <h1>Internal Server Error</h1>\n"
152                   << "  <p>" << e.getMessage() << "</p>\n"
153                   << "  <hr/>"
154                   << "  <address>suPHP " << PACKAGE_VERSION << "</address>\n"
155                   << " </body>\n"
156                   << "</html>\n";
157     }
158 }
159
160
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;
166 }
167
168
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!",
175                                 __FILE__, __LINE__);
176     }
177
178     if (!api.getEffectiveProcessUser().isSuperUser()) {
179         throw SecurityException(
180             "Do not have root privileges. Executable not set-uid root?",
181             __FILE__, __LINE__);
182     }
183 }
184
185
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());
194
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__);
200     }
201     if (!realScriptFile.exists()) {
202         std::string error = "File " + realScriptFile.getPath()
203             + " referenced by symlink " +scriptFile.getPath()
204             + " does not exist";
205         logger.logWarning(error);
206         throw SoftException(error, __FILE__, __LINE__);
207     }
208
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",
212                                 __FILE__, __LINE__);
213     if (config.getCheckVHostDocroot()
214         && realScriptFile.getPath().find(environment.getVar("DOCUMENT_ROOT"))
215         != 0) {
216
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__);
222     }
223     if (config.getCheckVHostDocroot()
224         && scriptFile.getPath().find(environment.getVar("DOCUMENT_ROOT"))
225         != 0) {
226
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__);
232     }
233
234     // Check script permissions
235     // Directories will be checked later
236     if (!realScriptFile.hasUserReadBit()) {
237         std::string error = "File \"" + realScriptFile.getPath()
238             + "\" not readable";
239         logger.logWarning(error);
240         throw SoftException(error, __FILE__, __LINE__);
241
242     }
243
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__);
250     }
251
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__);
258     }
259
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__);
267     }
268 }
269
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);
280
281     // Get full path to script file
282     File realScriptFile = File(scriptFile.getRealPath());
283
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;
291             break;
292         }
293     }
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__);
300     }
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;
306             break;
307         }
308     }
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__);
314     }
315
316     // Check directory ownership and permissions
317     checkParentDirectories(realScriptFile, targetUser, config);
318     checkParentDirectories(scriptFile, targetUser, config);
319 }
320
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) {
328
329     File scriptFile = File(scriptFilename);
330     File realScriptFile = File(scriptFile.getRealPath());
331     API& api = API_Helper::getSystemAPI();
332     Logger& logger = api.getSystemLogger();
333
334     // Make sure that exactly one mode is set
335
336 #if !defined(OPT_USERGROUP_OWNER) && !defined(OPT_USERGROUP_FORCE) && !defined(OPT_USERGROUP_PARANOID)
337 #error "No uid/gid change model specified"
338 #endif
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"
341 #endif
342
343     // Common code (for all security modes)
344
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__);
351     }
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__);
357     }
358
359     // Paranoid and force mode
360
361 #if (defined(OPT_USERGROUP_PARANOID) || defined(OPT_USERGROUP_FORCE))
362     std::string targetUsername, targetGroupname;
363     try {
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",
369             __FILE__, __LINE__);
370     }
371
372     if (targetUsername[0] == '#' && targetUsername.find_first_not_of(
373             "0123456789", 1) == std::string::npos) {
374         targetUser = api.getUserInfo(Util::strToInt(targetUsername.substr(1)));
375     } else {
376         targetUser = api.getUserInfo(targetUsername);
377     }
378
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)));
383     } else {
384         targetGroup = api.getGroupInfo(targetGroupname);
385     }
386 #endif // OPT_USERGROUP_PARANOID || OPT_USERGROUP_FORCE
387
388     // Owner mode only
389
390 #ifdef OPT_USERGROUP_OWNER
391     targetUser = scriptFile.getUser();
392     targetGroup = scriptFile.getGroup();
393 #endif // OPT_USERGROUP_OWNER
394
395     // Paranoid mode only
396
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__);
405     }
406
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__);
414     }
415 #endif // OPT_USERGROUP_PARANOID
416 }
417
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();
424
425     // Set new group first, because we still need super-user privileges
426     // for this
427     api.setProcessGroup(targetGroup);
428
429     // Then set new user
430     api.setProcessUser(targetUser);
431
432     api.setUmask(config.getUmask());
433 }
434
435
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;
441
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");
459
460     // Reset PATH
461     env.putVar("PATH", config.getEnvPath());
462
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"));
471         }
472
473         // PHP may need this, when compiled with security features
474         if (!env.hasVar("REDIRECT_STATUS")) {
475             env.putVar("REDIRECT_STATUS", "200");
476         }
477     }
478
479     return env;
480 }
481
482
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",
488                                 __FILE__, __LINE__);
489     std::string handler = env.getVar("SUPHP_HANDLER");
490
491     std::string interpreter = "";
492     try {
493         interpreter = config.getInterpreter(handler);
494     } catch (KeyNotFoundException& e) {
495         throw SecurityException ("Handler not found in configuration", e,
496                                  __FILE__, __LINE__);
497     }
498
499     return interpreter;
500 }
501
502
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;
509     else
510         throw SecurityException("Unknown Interpreter: " + interpreter,
511                                 __FILE__, __LINE__);
512 }
513
514
515 void suPHP::Application::executeScript(const std::string& scriptFilename,
516                                        const std::string& interpreter,
517                                        TargetMode mode,
518                                        const Environment& env,
519                                        const Configuration& config) const
520     throw (SoftException) {
521     try {
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);
527             CommandLine cline;
528             cline.putArgument(interpreterPath);
529             API_Helper::getSystemAPI().execute(interpreterPath, cline, env);
530         } else if (mode == TARGETMODE_SELFEXECUTE) {
531             CommandLine cline;
532             cline.putArgument(scriptFilename);
533             API_Helper::getSystemAPI().execute(scriptFilename, cline, env);
534         }
535     } catch (SystemException& e) {
536         throw SoftException("Could not execute script \"" + scriptFilename
537                                 + "\"", e, __FILE__, __LINE__);
538     }
539 }
540
541
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();
547     do {
548         directory = directory.getParentDirectory();
549
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__);
556         }
557
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__);
565         }
566
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__);
574         }
575     } while (directory.getPath() != "/");
576 }
577
578
579 int main(int argc, char **argv) {
580     try {
581         API& api = API_Helper::getSystemAPI();
582         CommandLine cmdline;
583         Environment env;
584         Application app;
585         for (int i=0; i<argc; i++) {
586             cmdline.putArgument(argv[i]);
587         }
588         env = api.getProcessEnvironment();
589         return app.run(cmdline, env);
590     } catch (Exception& e) {
591         std::cerr << e;
592         return 1;
593     }
594 }