Release
[hcoop/zz_old/debian/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 }