14 min read

Making a directory path safe

When we poke around in the server filesystem, we want to make sure that we know what we are doing. If we are dealing with directories, Joomla! provides us with some easy ways to make sure that those directories are being safely referenced. Failure to properly sanitize directory paths can lead to major security vulnerabilities. For example, if we were attempting to remove a directory, a security vulnerability could allow the deletion of the completely wrong resource! For more information about external control of paths, refer to CWE-73.

A directory path is a URL to a directory. A directory path does not include a file. To safely manage a path to a file, refer to the next recipe, Making a path safe.

How to do it…

The static JFolder class, which is a part of the joomla.filesystem library, provides us with all sorts of useful methods for working with folders. To use the class, we must import it.

jimport('joomla.filesystem.folder');

This bit is nice and easy. We use the JFolder::makeSafe() method and pass the name of the folder we want to sanitize. This method returns a string that can be used to interact safely with a folder.

// make the directory path safe
$safeDirPath = JFolder::makeSafe($unsafeDirPath);

The one downside of JFolder::makeSafe() is that it assumes that the directory separators are correctly defined in the original string. For example, while running on a *nix system, if the string contained Windows-style backslashes instead of *nix style forward slashes, those slashes would be stripped. We can use the static JPath class to overcome this, as follows:

// import JPath and JFolder
jimport('joomla.filesystem.path');
jimport('joomla.filesystem.folder');
// clean the path
$cleanDirPath = JPath::clean($unsafeDirPath);
// make the directory path safe
$safeDirPath = JFolder::makeSafe($cleanDirPath);

This time we have included the JPath::clean() method prior to making the path safe. For more information about JPath, refer to the next recipe, Making a path safe.

Directory separators

The correct way to add directory separators in Joomla! is to use the DS constant. For example, the path to bar from foo is expressed as ‘foo’ . DS . ‘bar’.

How it works…

What exactly does the JFolder::makeSafe() method do to guarantee that the directory path is safe? It strips out any characters or character sequences that are seen as posing potential security risks. The following list describes the characters that are considered safe:

  • Alphanumeric (a-z, A-Z, and 0-9)
  • Colon
  • Dash
  • Directory separators—the exact character sequence will depend on the environment
  • Space
  • Underscore

The following table shows some examples of input and output strings from the JFolder::makeSafe() and JPath::clean() methods running on a Windows system (DS == ”). This is intended to show why using the two together can be preferable. If we choose to use the two together, order of usage is important. We should always use the JPath::clean() method first and the JFolder::makeSafe() method next. Although, as the fourth example shows, sometimes it can be worth cleaning a second time, which means clean, make safe, and clean.

 

Original

 

Clean

 

Safe

 

Clean and safe

 

foobar

 

foobar

 

foobar

 

foobar

 

//foo/bar

 

foobar

 

foobar

 

foobar

 

foo bar

 

foo bar

 

foo20bar

 

foo20bar

 

foo bar/..

 

foo bar..

 

foo bar

 

foo bar

 

/foo”

 

foo”

 

foodel

 

foodel

 

del *.*

 

del *.*

 

 

 

See also

For information about safely dealing with files, refer to the previous recipe, Making a filename safe. For information about dealing with paths, please refer to the next recipe, Making a path safe.

Making a path safe

This recipe is similar to the previous two recipes, Making a filename safe and Making a directory path safe. This recipe differs in that it is for a complete path, normally to a file. There is a whole raft of security issues associated with processing paths. The following list identifies some of the more common issues we need to be aware of:

  • CWE-22: Path traversal
  • CWE-73: External control of filename or path
  • CWE-98: Insufficient control of filename for include/require statement in PHP program (aka ‘PHP file inclusion’)
  • CWE-434: Unrestricted file upload

All of these vulnerabilities can have serious consequences, which should not be overlooked. For example, a malicious user could upload a destructively tailored script file and then execute it. Luckily, Joomla! provides us with some easy ways to reduce the risks associated with these potential weaknesses.

Getting ready

Before we delve into some of the complex ways of safely dealing with paths, let’s start small. If we browse a basic installation of Joomla!, we will discover a large number of empty index.html files. These files prevent directory listings. Most web servers automatically generate directory listings if we visit a directory in which there are no index files. We should always add a copy of the empty index.html file to every directory in our extension.

The use of empty index.html files provides a form of security through obscurity. This is only intended to be a very basic safeguard and should never be relied on for complete protection. For more information, refer to CWE-656.

The second thing we should do is ensure that all of our PHP files can only be executed if the _JEXEC constant has been defined. This is used to make sure that the file has been executed from within Joomla!, that is, make sure it is not being used as a standalone script or is included from a script other than Joomla!

// Check file executed from within Joomla!
defined('_JEXEC') or die('Restricted access');

How to do it…

If we are retrieving a path value from a request variable, we can use the PATH type. This type is not entirely what it seems, as a PATH type cannot be an absolute path and cannot reference hidden files and folders. This means it cannot start with any form of directory separator, as it would in a *nix environment, or a drive identifier, as it would in a Windows environment. It also means that none of the folder names or the optional filename can start with a period (a *nix hidden file/folder). If a value does not reach these criteria, the return value will be null. Therefore, it is very important to consider the suitability of the PATH type before opting to use it.

// get the value of myPath from the request
$myPath = JRequest::getVar('myPath', 'default', 'REQUEST', 'PATH');

On its own, the PATH type does not really constitute a security measure. For example, it does not protect against the path traversal CWE-22.

Once we have our path variable, it is time to clean it. Cleaning is done in Joomla! using the static JPath class. Cleaning resolves any issues with directory separators. The exact process depends on the filesystem directory separator, for example a back or forward slash. The point is that if the directory separators in the string are incorrect, they are corrected as necessary.

// import the JPath class
jimport('joomla.filesystem.path');
// clean $myPath
$myPath = JPath::clean($myPath);

Like the PATH type, a cleaned path does not really constitute a security measure. For example, it too does not protect against path traversal CWE-22.

OK, we’ve heard enough about not constituting a security measure. Now it’s time to overcome that problem. The static JPath class includes the JPath::check() method, which checks for path traversal and also that the path is within the Joomla! installation. The only constraint is that the method can only deal with absolute paths. Remember the PATH type used in JRequest can only cope with relative paths. Therefore, if we use the PATH type, we must convert it to an absolute path before using the JPath::check() method.

// check for path traversal and snooping
JPath::check($myPath);

The odd thing about this method is that we don’t really do anything with the result! There is a very good reason for this. If the check fails, Joomla! will exit and display a suitable error message. In some instances, this may not be appropriate. Unfortunately, there is nothing we can do to prevent this. Therefore, if we want to avoid this we will have to check the path ourselves. Generally speaking, if a path fails the check, it is likely that an attack has been attempted. For that reason, exiting Joomla! is probably the most suitable response.

However, the JPath::check() method does have one serious limitation. It only checks for snooping outside of the Joomla! root directory. We can manually check that we are only looking in a specified area in the Joomla! installation.

// create path which must be the root of the directory
$safePath = JPATH_COMPONENT . DS . 'safeFolder' . DS;
// check for snooping outside of $safePath
if (strpos($myPath, $safePath) !== 0) {
JError::raiseError(20, 'Snooping out of bounds');
jexit();
}
// check for file traversal
jimport('joomla.filesystem.path');
JPath::check($directory);

Essentially, this only ensures that the start of the $myPath string is equivalent to $safePath. We deal with failures in the same way as the JPath::check() method. Notice that we still use the JPath::check() method because we can still effectively use this method to check for file traversal.

See also

The previous two recipes, Making a filename safe and Making a directory path safe, discuss how to work safely with filenames and paths.

We can also use filesystem permissions to secure files and folders.

Safely retrieving request data

Almost 99% of the time, security vulnerabilities in PHP applications such as Joomla! are caused by inadequate input parsing and validation CWE-20. We access request data in Joomla! using the static JRequest class. Built into this class is the ability to cast values to specific types and to mask data.

We cannot rely solely on JRequest to ensure that incoming data is safe. This is where validation comes into play. Input data always has definable constraints. For example, we might define an entity identifier as a positive integer no less than 1 and no greater than 4294967295 (the maximum value for an unsigned MySQL INT). If we can define the constraints, we can also check that the input adheres to the constraints.

Validating strings tend to be more complex. This is because strings are highly versatile and can contain many different characters. One thing we should always bear in mind when dealing with string validation is the effect of different character encodings. Joomla! 1.5 is UTF-8 compliant. UTF-8 is a Unicode variable-size multibyte character encoding that enables the encoding of many different alphabets and symbols that would otherwise be unavailable. As PHP is not UTF-8 aware, we should always use the static JString methods instead of the PHP string functions when dealing with UTF-8 strings.

Getting ready

Prior to doing anything, it is worth defining and documenting the boundaries of all the input that we use in our extension. This may include value ranges and formats. This can be a lengthy task, but it will help to ensure that our extension is secure.

How to do it…

The most important thing we must do when accessing request data is to use JRequest. Even if we just want the raw values, JRequest forms an important part of the Joomla! framework. Even before we get a chance to execute any code in our extension, JRequest will have already performed vital security work in an attempt to prevent global variable injection, CWE-471.

So where do we begin? We start with the simple JRequest::getVar() method. This method is used to safely get at the request data. There are five parameters, of which only the first is required. The following example shows how we use the first three of these parameters:

// gets the value of name
$value = JRequest::getVar('nameOfVar');
// gets the value of name,
// if name is not specified returns defaultValue
$value = JRequest::getVar('nameOfVar', 'defaultValue');
// gets the value of name,
// if name is not specified returns defaultValue
// name is retrieved from the GET request data
$value = JRequest::getVar('nameOfVar', 'defaultValue', 'GET');

The third parameter can be any of the following values:

 

Value

Description

COOKIE

HTTP Cookies

ENV

Environment variables

FILES

Uploaded file details

GET

HTTP GET variables

POST

HTTP POST variables

REQUEST

Combination of GET, POST, and COOKIE; this is the default

SERVER

Server and environment variables

In most instances, REQUEST (the default) should be sufficient. The only time we need to use GET and POST explicitly is when we are expecting a request to use a specific HTTP method. For added security, we can restrict a request to a certain HTTP method using the static JRequest::getMethod() method, as shown in the following example:

if (JRequest::getMethod() != 'GET') {
jexit('UNEXPECTED REQUEST METHOD');
}

There’s more…

The following two subsections discuss the last two parameters, $type and $mask. It is in the last two parameters where the security benefits of JRequest become very apparent.

Casting

Strictly speaking, casting is not an accurate description of the fourth JRequest::getVar() parameter. The types that we can cast to are not the types that we would use to describe a variable. For example, WORD is not a PHP or Joomla! type, but it is an available option.

The following example extracts an integer representation of the GET request value, nameOfVar:

// gets the value of nameOfVar,
// if nameOfVar is not specified returns 0
// name is retrieved from the GET request data
// casts the return value as an integer
$int = JRequest::getVar('nameOfVar', 0, 'GET', 'INT');

Hey presto! We have a safe value that we know is an integer. Had we not included the parameter to cast the value, we would not have been able to guarantee that $int was in fact an integer. Of course, we could have used the PHP intval() function or cast the value ourselves using (int). Not all of the types we can cast to are as simple as an integer; for example, ALNUM is used to strip non-alphanumeric characters from a string value.

JRequest also provides alias methods that allow us to achieve the same thing, but with less code. For example, we can quickly extract an integer. The following example is the same as the previous example:

$int = JRequest::getInt('nameOfVar', 0);

The following table describes all of the types we can cast to using JRequest. Note that only the most commonly used types have alias methods.

Type

 

Description

 

Alias

 

DEFAULT

 

Aggressive cleaning occurs to remove all detected code elements

 

 

ALNUM

 

Alphanumeric string; strips all non-ASCII letters and numbers

 

 

ARRAY

 

Force array cast

 

 

BASE64

 

Base64 string; strips all non Base64 characters, which is useful for passing encoded data in a URL, for example a return URL

 

 

BOOL or BOOLEAN

 

Force Boolean cast

 

getBool()

 

CMD

 

Command; strips all non-alphanumeric, underscore, period, and dash characters, which is ideal for values such as task

 

getCmd()

 

FLOAT or DOUBLE

 

Floating point number

 

getFloat()

 

INT or INTEGER

 

Whole number

 

getInt()

 

PATH

 

Filesystem path; used to identify a resource in a filesystem, for example the path to an image to use as a logo (relative paths only)

 

 

STRING

 

String; often used with a mask to clean the data

 

getString()

 

USERNAME

 

Username; strips characters unsuitable for use in a username, including non-printing characters (for example a backspace), angled brackets, double and single quotation marks, percent signs, and ampersands

 

 

WORD

 

Word; strips all non-alpha and underscore characters

 

getWord()

 

 

By default, the most aggressive mask is applied. This will remove code, such as HTML and JavaScript, from the data. The next section describes how we use masks.

Masking strings

The fifth JRequest::getVar() parameter defines a mask. Masks are used in JRequest to define what is and isn’t allowed in a string value. By default no masking is applied, which means that very aggressive security measures are taken to ensure that the incoming data is as safe as possible. While this is useful, it is not always appropriate. For example, the core component com_content allows users to enter HTML data in an article. Without a mask, this information would be stripped from the request data.

There are three constants we can use to easily define the mask we want to apply. These are JREQUEST_NOTRIM, JREQUEST_ALLOWRAW, and JREQUEST_ALLOWHTML. The following example shows how we can use these:


// using getVar
$string = JRequest::getVar('nameOfVar', 'default', 'REQUEST',
'STRING', JREQUEST_ALLOWRAW);
// using getString alias
$string = JRequest::getString('nameOfVar', 'default', 'REQUEST',
JREQUEST_ALLOWRAW);

The following examples show how the output varies depending on the mask that we apply:

#

Original input value

1

<p>Paragraph <a onClick=”alert(‘foobar’);”>link</a></p>

2

CSS <link type=”text/css”, href=”http://somewhere/nasty.css” />

3

space at front of input

4

&ltp&gtPara&lt/p&gt

#

Output value (No mask)

1

Paragraph link

2

CSS

3

space at front of input

4

&ltp&gtPara&lt/p&gt

 

 


LEAVE A REPLY

Please enter your comment!
Please enter your name here