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