0.6.0-1 release
[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         this->changeProcessPermissions(scriptFilename, config, env);
93
94         interpreter = this->getInterpreter(env, config);
95         targetMode = this->getTargetMode(interpreter);
96         
97         // Prepare environment for new process
98         newEnv = this->prepareEnvironment(env, config, targetMode);
99         
100         // Log attempt to execute script
101         logger.logInfo("Executing \"" + scriptFilename + "\" as UID "
102                        + Util::intToStr(api.getEffectiveProcessUser().getUid())
103                        + ", GID " 
104                        + Util::intToStr(
105                            api.getEffectiveProcessGroup().getGid()));
106
107         this->executeScript(scriptFilename, interpreter, targetMode, newEnv, 
108                             config);
109         
110         // Function should never return
111         // So, if we get here, return with error code
112         return 1;
113     } catch (SoftException& e) {
114         if (!config.getErrorsToBrowser()) {
115             std::cerr << e;
116             return 2;
117         }
118         std::cout << "Content-Type: text/html\n"
119                   << "Status: 500\n"
120                   << "\n"
121                   << "<html>\n"
122                   << " <head>\n"
123                   << "  <title>500 Internal Server Error</title>\n"
124                   << " </head>\n"
125                   << " <body>\n"
126                   << "  <h1>Internal Server Error</h1>\n"
127                   << "  <p>" << e.getMessage() << "</p>\n"
128                   << "  <hr/>"
129                   << "  <address>suPHP " << PACKAGE_VERSION << "</address>\n"
130                   << " </body>\n"
131                   << "</html>\n";
132     }
133 }
134
135
136 void suPHP::Application::printAboutMessage() {
137     std::cerr << "suPHP version " << PACKAGE_VERSION << "\n";
138     std::cerr << "(c) 2002-2005 Sebastian Marsching\n";
139     std::cerr << std::endl;
140     std::cerr << "suPHP has to be called by mod_suphp to work." << std::endl;
141 }
142
143
144 void suPHP::Application::checkProcessPermissions(Configuration& config) 
145     throw (SecurityException, LookupException) {
146     API& api = API_Helper::getSystemAPI();
147     if (api.getRealProcessUser() !=
148         api.getUserInfo(config.getWebserverUser())) {
149         throw SecurityException("Calling user is not webserver user!",
150                                 __FILE__, __LINE__);
151     }
152     
153     if (!api.getEffectiveProcessUser().isSuperUser()) {
154         throw SecurityException(
155             "Do not have root privileges. Executable not set-uid root?",
156             __FILE__, __LINE__);
157     }
158 }
159
160
161 void suPHP::Application::checkScriptFile(
162     const std::string& scriptFilename, 
163     const Configuration& config,
164     const Environment& environment) const
165     throw (SystemException, SoftException) {
166     Logger& logger = API_Helper::getSystemAPI().getSystemLogger();
167     File scriptFile = File(scriptFilename);
168
169     // Check wheter file exists
170     if (!scriptFile.exists()) {
171         std::string error = "File " + scriptFile.getPath() + " does not exist";
172         logger.logWarning(error);
173         throw SoftException(error, __FILE__, __LINE__);
174     }
175     
176     // Get full path to script file
177
178     File realScriptFile = File(scriptFile.getRealPath());
179     File directory = realScriptFile.getParentDirectory();
180     
181     // Check wheter script is in docroot
182     if (realScriptFile.getPath().find(config.getDocroot()) != 0) {
183         std::string error = "Script \"" + scriptFile.getPath() 
184             + "\" resolving to \"" + realScriptFile.getPath() 
185             + "\" not within configured docroot";
186         logger.logWarning(error);
187         throw SoftException(error, __FILE__, __LINE__);
188     }
189
190     // If enabled, check whether script is in the vhost's docroot
191     if (!environment.hasVar("DOCUMENT_ROOT"))
192         throw SoftException("Environment variable DOCUMENT_ROOT not set",
193                                 __FILE__, __LINE__);
194     if (config.getCheckVHostDocroot()
195         && realScriptFile.getPath().find(environment.getVar("DOCUMENT_ROOT")) 
196         != 0) {
197         
198         std::string error = "File \"" + realScriptFile.getPath()
199             + "\" is not in document root of Vhost \""
200             + environment.getVar("DOCUMENT_ROOT") + "\"";
201         logger.logWarning(error);
202         throw SoftException(error, __FILE__, __LINE__);
203     }
204
205     // Check script and directory permissions
206     if (!realScriptFile.hasUserReadBit()) {
207         std::string error = "File \"" + realScriptFile.getPath()
208             + "\" not readable";
209         logger.logWarning(error);
210         throw SoftException(error, __FILE__, __LINE__);
211         
212     }
213
214     if (!config.getAllowFileGroupWriteable()
215         && realScriptFile.hasGroupWriteBit()) {
216         std::string error = "File \"" + realScriptFile.getPath()
217             + "\" is writeable by group";
218         logger.logWarning(error);
219         throw SoftException(error, __FILE__, __LINE__);
220     }
221     
222     if (!config.getAllowDirectoryGroupWriteable() 
223         && directory.hasGroupWriteBit()) {
224         std::string error = "Directory \"" + directory.getPath()
225             + "\" is writeable by group";
226         logger.logWarning(error);
227         throw SoftException(error, __FILE__, __LINE__);
228     }
229
230     if (!config.getAllowFileOthersWriteable()
231         && realScriptFile.hasOthersWriteBit()) {
232         std::string error = "File \"" + realScriptFile.getPath()
233             + "\" is writeable by others";
234         logger.logWarning(error);
235         throw SoftException(error, __FILE__, __LINE__);
236     }
237     
238     if (!config.getAllowDirectoryOthersWriteable()
239         && directory.hasOthersWriteBit()) {
240         std::string error = "Directory \"" + directory.getPath()
241             + "\" is writeable by others";
242         logger.logWarning(error);
243         throw SoftException(error, __FILE__, __LINE__);
244     }
245
246     // Check UID/GID of symlink is matching target
247     if (scriptFile.getUser() != realScriptFile.getUser()
248         || scriptFile.getGroup() != realScriptFile.getGroup()) {
249         std::string error = "UID or GID of symlink \"" + scriptFile.getPath() 
250             + "\" is not matching its target";
251         logger.logWarning(error);
252         throw SoftException(error, __FILE__, __LINE__);
253     }
254 }
255
256
257 void suPHP::Application::changeProcessPermissions(
258     const std::string& scriptFilename, 
259     const Configuration& config,
260     const Environment& environment) const
261     throw (SystemException, SoftException, SecurityException) {
262     UserInfo targetUser;
263     GroupInfo targetGroup;
264
265     File scriptFile = File(File(scriptFilename).getRealPath());
266     API& api = API_Helper::getSystemAPI();
267     Logger& logger = api.getSystemLogger();
268
269     // Make sure that exactly one mode is set
270
271 #if !defined(OPT_USERGROUP_OWNER) && !defined(OPT_USERGROUP_FORCE) && !defined(OPT_USERGROUP_PARANOID)
272 #error "No uid/gid change model specified"
273 #endif
274 #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))
275 #error "More than one uid/gid change model specified"
276 #endif
277
278     // Common code (for all security modes)
279
280     // Check UID/GID of script
281     if (scriptFile.getUser().getUid() < config.getMinUid()) {
282         std::string error = "UID of script \"" + scriptFilename
283             + "\" is smaller than min_uid";
284         logger.logWarning(error);
285         throw SoftException(error, __FILE__, __LINE__);
286     }
287     if (scriptFile.getGroup().getGid() < config.getMinGid()) {
288         std::string error = "GID of script \"" + scriptFilename
289             + "\" is smaller than min_gid";
290         logger.logWarning(error);
291         throw SoftException(error, __FILE__, __LINE__);
292     }
293     
294     // Paranoid and force mode
295
296 #if (defined(OPT_USERGROUP_PARANOID) || defined(OPT_USERGROUP_FORCE))
297     std::string targetUsername, targetGroupname;
298     try {
299         targetUsername = environment.getVar("SUPHP_USER");
300         targetGroupname = environment.getVar("SUPHP_GROUP");
301     } catch (KeyNotFoundException& e) {
302         throw SecurityException(
303             "Environment variable SUPHP_USER or SUPHP_GROUP not set", 
304             __FILE__, __LINE__);
305     }
306     
307     if (targetUsername[0] == '#' && targetUsername.find_first_not_of(
308             "0123456789", 1) == std::string::npos) {
309         targetUser = api.getUserInfo(Util::strToInt(targetUsername.substr(1)));
310     } else {
311         targetUser = api.getUserInfo(targetUsername);
312     }
313
314     if (targetGroupname[0] == '#' && targetGroupname.find_first_not_of(
315             "0123456789", 1) == std::string::npos) {
316         targetGroup = api.getGroupInfo(
317             Util::strToInt(targetGroupname.substr(1)));
318     } else {
319         targetGroup = api.getGroupInfo(targetGroupname);
320     }
321 #endif // OPT_USERGROUP_PARANOID || OPT_USERGROUP_FORCE
322
323     // Owner mode only
324
325 #ifdef OPT_USERGROUP_OWNER
326     targetUser = scriptFile.getUser();
327     targetGroup = scriptFile.getGroup();
328 #endif // OPT_USERGROUP_OWNER
329     
330     // Paranoid mode only
331
332 #ifdef OPT_USERGROUP_PARANOID
333     if (targetUser != scriptFile.getUser()) {
334         std::string error ="Mismatch between target UID ("
335             + Util::intToStr(targetUser.getUid()) + ") and UID (" 
336             + Util::intToStr(scriptFile.getUser().getUid()) + ") of file \"" 
337             + scriptFile.getPath() + "\"";
338         logger.logWarning(error);
339         throw SoftException(error, __FILE__, __LINE__);
340     }
341
342     if (targetGroup != scriptFile.getGroup()) {
343         std::string error ="Mismatch between target GID ("
344             + Util::intToStr(targetGroup.getGid()) + ") and GID (" 
345             + Util::intToStr(scriptFile.getGroup().getGid()) + ") of file \"" 
346             + scriptFile.getPath() + "\"";
347         logger.logWarning(error);
348         throw SoftException(error, __FILE__, __LINE__);
349     }
350 #endif // OPT_USERGROUP_PARANOID    
351
352     // Common code used for all modes
353
354     // Set new group first, because we still need super-user privileges
355     // for this
356     api.setProcessGroup(targetGroup);
357     
358     // Then set new user
359     api.setProcessUser(targetUser);
360
361     api.setUmask(config.getUmask());
362 }
363
364
365 Environment suPHP::Application::prepareEnvironment(
366     const Environment& sourceEnv, const Configuration& config, TargetMode mode)
367     throw (KeyNotFoundException) {
368     // Create environment for new process from old environment
369     Environment env = sourceEnv;
370     
371     // Delete unwanted environment variables
372     if (env.hasVar("LD_PRELOAD"))
373         env.deleteVar("LD_PRELOAD");
374     if (env.hasVar("LD_LIBRARY_PATH"))
375         env.deleteVar("LD_LIBRARY_PATH");
376     if (env.hasVar("SUPHP_USER"))
377         env.deleteVar("SUPHP_USER");
378     if (env.hasVar("SUPHP_GROUP"))
379         env.deleteVar("SUPHP_GROUP");
380     if (env.hasVar("SUPHP_HANDLER"))
381         env.deleteVar("SUPHP_HANDLER");
382     if (env.hasVar("SUPHP_AUTH_USER"))
383         env.deleteVar("SUPHP_AUTH_USER");
384     if (env.hasVar("SUPHP_AUTH_PW"))
385         env.deleteVar("SUPHP_AUTH_PW");
386     if (env.hasVar("SUPHP_PHP_CONFIG"))
387         env.deleteVar("SUPHP_PHP_CONFIG");
388     
389     // Reset PATH
390     env.putVar("PATH", config.getEnvPath());
391
392     // If we are in PHP mode, set PHP specific variables
393     if (mode == TARGETMODE_PHP) {
394         if (sourceEnv.hasVar("SUPHP_PHP_CONFIG"))
395             env.putVar("PHPRC", sourceEnv.getVar("SUPHP_PHP_CONFIG"));
396         if (sourceEnv.hasVar("SUPHP_AUTH_USER")
397             && sourceEnv.hasVar("SUPHP_AUTH_PW")) {
398             env.putVar("PHP_AUTH_USER", sourceEnv.getVar("SUPHP_AUTH_USER"));
399             env.putVar("PHP_AUTH_PW", sourceEnv.getVar("SUPHP_AUTH_PW"));
400         }
401
402         // PHP may need this, when compiled with security features
403         if (!env.hasVar("REDIRECT_STATUS")) {
404             env.putVar("REDIRECT_STATUS", "200");
405         }
406     }
407     
408     return env;
409 }
410
411
412 std::string suPHP::Application::getInterpreter(
413     const Environment& env, const Configuration& config)
414     throw (SecurityException) {
415     if (!env.hasVar("SUPHP_HANDLER"))
416         throw SecurityException("Environment variable SUPHP_HANDLER not set",
417                                 __FILE__, __LINE__);
418     std::string handler = env.getVar("SUPHP_HANDLER");
419     
420     std::string interpreter = "";
421     try {
422         interpreter = config.getInterpreter(handler);
423     } catch (KeyNotFoundException& e) {
424         throw SecurityException ("Handler not found in configuration", e,
425                                  __FILE__, __LINE__);
426     }
427     
428     return interpreter;
429 }
430
431
432 TargetMode suPHP::Application::getTargetMode(const std::string& interpreter)
433     throw (SecurityException) {
434     if (interpreter.substr(0, 4) == "php:")
435         return TARGETMODE_PHP;
436     else if (interpreter == "execute:!self")
437         return TARGETMODE_SELFEXECUTE;
438     else
439         throw SecurityException("Unknown Interpreter: " + interpreter,
440                                 __FILE__, __LINE__);
441 }
442
443
444 void suPHP::Application::executeScript(const std::string& scriptFilename,
445                                        const std::string& interpreter,
446                                        TargetMode mode,
447                                        const Environment& env,
448                                        const Configuration& config) const
449     throw (SoftException) {
450     try {
451         // Change working directory to script path
452         API_Helper::getSystemAPI().setCwd(
453             File(scriptFilename).getParentDirectory().getPath());
454         if (mode == TARGETMODE_PHP) {
455             std::string interpreterPath = interpreter.substr(4);
456             CommandLine cline;
457             cline.putArgument(interpreterPath);
458             API_Helper::getSystemAPI().execute(interpreterPath, cline, env);
459         } else if (mode == TARGETMODE_SELFEXECUTE) {
460             CommandLine cline;
461             cline.putArgument(scriptFilename);
462             API_Helper::getSystemAPI().execute(scriptFilename, cline, env);
463         }
464     } catch (SystemException& e) {
465         throw SoftException("Could not execute script \"" + scriptFilename
466                                 + "\"", e, __FILE__, __LINE__);
467     }
468 }
469
470
471 int main(int argc, char **argv) {
472     try {
473         API& api = API_Helper::getSystemAPI();
474         CommandLine cmdline;
475         Environment env;
476         Application app;
477         for (int i=0; i<argc; i++) {
478             cmdline.putArgument(argv[i]);
479         }
480         env = api.getProcessEnvironment();
481         return app.run(cmdline, env);
482     } catch (Exception& e) {
483         std::cerr << e;
484         return 1;
485     }
486 }