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