加载中…
正文 字体大小:

habits for writing secure PHP applications1

(2008-10-18 18:01:12)
标签:

杂谈

原地址
Security in a PHP application includes remote and local security concerns. Discover the habits PHP developers should get into to implement Web applications that have both characteristics.

When it comes to security, remember that in addition to actual platform and operating system security issues, you need to ensure that you write your application to be secure. When you write PHP applications, apply these seven habits to make sure your applications are as secure as possible:

  • Validate input
  • Guard your file system
  • Guard your database
  • Guard your session data
  • Guard against Cross-Site Scripting (XSS) vulnerabilities
  • Verify form posts
  • Protect against Cross-Site Request Forgeries (CSRF)

Validate input

Validating data is the most important habit you can possibly adopt when it comes to security. And when it comes to input, it's simple: Don't trust users. Your users are probably good people, and most are likely to use your application exactly as you intended. However, whenever there is chance for input, there is also chance for really, really bad input. As an application developer, you must guard your application against bad input. Carefully considering where your user input is going and what it should be will allow you to build a robust, secure application.

Although file system and database interaction are covered later, there are general validation tips that cover every sort of validation:

  • Use white-listed values
  • Always revalidate limited selections
  • Use built-in escape functions
  • Validate for correct data types, like numbers

White-listed values are values that are valid, as opposed to black-listed values that are invalid. The distinction is that often when doing validation, the list or range of possible values is smaller than the list of invalid values, many of which can be unknown or unexpected.

When you're doing validation, remember that it's often easier to conceptualize and validate what the application allows instead of trying to guard against all the unknown values. For instance, to limit values in a field to all numbers, write a routine that makes sure the input is all numbers. Don't write the routine to search for non-numerical values and mark it as invalid if any are found.

Guard your file system

In July 2000, a Web site leaked customer data that was found in files on a Web server. A visitor to the Web site manipulated the URL to view files containing the data. Although the files were erroneously placed, this example underscores the importance of guarding your file system against attackers.

If your PHP application does anything with files and has variable data that a user can enter, be careful that you scrub the user input to make sure users can't do anything with the file system that you don't want them to do. Listing 1 shows an example of a PHP site that downloads an image given a name.


Listing 1. Downloading a file
<?php
if ($_POST['submit'] == 'Download') {
    $file = $_POST['fileName'];
    header("Content-Type: application/x-octet-stream");
    header("Content-Transfer-Encoding: binary");
    header("Content-Disposition: attachment; filename="" . $file . "";" );
    $fh = fopen($file, 'r');
    while (! feof($fh))
    {
        echo(fread($fh, 1024));
    }
    fclose($fh);
} else {
    echo("<html><head><");
        echo("title>Guard your filesystem</title></head>");
    echo("<body><form id="myFrom" action="" . $_SERVER['PHP_SELF'] .
        "" method="post">");
    echo("<div><input type="text" name="fileName" value="");
    echo(isset($_REQUEST['fileName']) ? $_REQUEST['fileName'] : '');
    echo("" />");
    echo("<input type="submit" value="Download" name="submit" /></div>");
    echo("</form></body></html>");
}

As you can see, the relatively dangerous script in Listing 1 serves any file that the Web server has read access to, including files in the session directory (see "Guard your session data") and even some system files such as /etc/passwd. This example has a text box in which the user can type the file name for example purposes, but the file name could just as easily be supplied in the query string.

Configuring file system access along with user input is dangerous, so it's best to avoid it altogether by designing your application to use a database and hidden, generated file names. However, that's not always possible. Listing 2 provides an example of a routine that validates file names. It uses regular expressions to make sure that only valid characters are used in the file name and checks specifically for the dot-dot characters: ...


Listing 2. Checking for valid file name characters
function isValidFileName($file) {
    
    return preg_match('/^(((?:\.)(?!\.))|\w)+$/', $file);
}



Back to top


Guard your database

In April 2008, a U.S. state's Department of Corrections leaked sensitive data as a result of the SQL column names being used in the query string. This leak allowed malicious users to select which columns they wanted to display, submit the page, and get the data. This leak shows how users can figure out ways to make their input do things that the application developers definitely didn't foresee and underscores the need for careful defense against SQL injection attacks.

Listing 3 shows an example of a script that runs an SQL statement. In this example, the SQL statement is a dynamic statement that would allow the same attack. Owners of this form may be tempted to think they're safe because they've limited the column names to select lists. However, the code neglects attention to the last habit regarding form spoofing — just because the code limits the selection to drop-down boxes doesn't mean that someone can't post a form with whatever they want in it (including an asterisk [*]).


Listing 3. Executing an SQL statement
<html>
<head>
<title>SQL Injection Example</title>
</head>
<body>
<form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>"
    method="post">
<div><input type="text" name="account_number"
    value="<?php echo(isset($_POST['account_number']) ? 
        $_POST['account_number'] : ''); ?>" />
<select name="col">
<option value="account_number">Account Number</option>
<option value="name">Name</option>
<option value="address">Address</option>
</select>
<input type="submit" value="Save" name="submit" /></div>
</form>
<?php
if ($_POST['submit'] == 'Save') {
    
    $link = mysql_connect('hostname', 'user', 'password') or 
        die ('Could not connect' . mysql_error());
    mysql_select_db('test', $link);
                
                $col = $_POST['col'];
    $select = "SELECT " . $col . " FROM account_data WHERE account_number = " 
        . $_POST['account_number'] . ";" ;
    echo '<p>' . $select . '</p>';
    $result = mysql_query($select) or die('<p>' . mysql_error() . '</p>');
    echo '<table>';
    while ($row = mysql_fetch_assoc($result)) {
        echo '<tr>';
        echo '<td>' . $row[$col] . '</td>';
        echo '</tr>';
    }
    echo '</table>';
    mysql_close($link);
}
?>
</body>
</html>

So, to form the habit of guarding your database, avoid dynamic SQL code as much as possible. If you can't avoid dynamic SQL code, don't use input directly for columns. Listing 4 shows an example of the power of adding a simple validation routine to the account number field to make sure it cannot be a non-number in addition to using static columns.


Listing 4. Guarding with validation and mysql_real_escape_string()
       
<html>
<head>
<title>SQL Injection Example</title>
</head>
<body>
<form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>"
    method="post">
<div><input type="text" name="account_number"
    value="<?php echo(isset($_POST['account_number']) ? 
        $_POST['account_number'] : ''); ?>" /> <input type="submit"
    value="Save" name="submit" /></div>
</form>
<?php
function isValidAccountNumber($number) 
{
    return is_numeric($number);
}
if ($_POST['submit'] == 'Save') {
    
    if (isset($_POST['account_number']) &&
    isValidAccountNumber($_POST['account_number'])) {
        
        $link = mysql_connect('hostname', 'user', 'password') or
        die ('Could not connect' . mysql_error());
        mysql_select_db('test', $link);
        $select = sprintf("SELECT account_number, name, address " .
                " FROM account_data WHERE account_number = %s;",
        mysql_real_escape_string($_POST['account_number']));
        echo '<p>' . $select . '</p>';
        $result = mysql_query($select) or die('<p>' . mysql_error() . '</p>');
        echo '<table>';
        while ($row = mysql_fetch_assoc($result)) {
            echo '<tr>';
            echo '<td>' . $row['account_number'] . '</td>';
            echo '<td>' . $row['name'] . '</td>';
            echo '<td>' . $row['address'] . '</td>';
            echo '</tr>';
        }
        echo '</table>';
        mysql_close($link);
    } else {
        echo "<span style="font-color:red">" .
    "Please supply a valid account number!</span>";
    }
}
?>
</body>
</html>

This example also shows the use of the mysql_real_escape_string() function. This function properly scrubs your input so it doesn't include invalid characters. If you've been relying on magic_quotes_gpc, be forewarned that it is deprecated and will be removed in PHP V6. Avoid relying on it now and write your PHP applications to be secure without it. Also, remember that if you're using an ISP, there's a chance that your it doesn't have magic_quotes_gpc enabled.

Finally, in the improved example, you can see that the SQL statement and output do not include a dynamic column selection. This way, if you add columns to the table later that have different information, you can print them. If you're using a framework to work with your database, there is a chance that your framework does the SQL validation for you already. Make sure to check with the documentation for your framework to be sure; if you're still unsure, do the validation to err on the safe side. Even if you're using a framework for database interaction, you still need to perform the other verification.



Back to top


Guard your session

By default, session information in PHP is written to a temporary directory. Consider the form in Listing 5, which shows how to store a user's ID and account number in a session.


Listing 5. Storing data in session
<?php
session_start();
?>
<html>
<head>
<title>Storing session information</title>
</head>
<body>
<?php
if ($_POST['submit'] == 'Save') {
    $_SESSION['userName'] = $_POST['userName'];
    $_SESSION['accountNumber'] = $_POST['accountNumber'];
}
?>
<form id="myFrom" action="<?php echo $_SERVER['PHP_SELF']; ?>"
    method="post">
<div><input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="text" name="userName"
    value="<?php echo(isset($_POST['userName']) ? $_POST['userName'] : ''); ?>" />
<br />
<input type="text" name="accountNumber"
    value="<?php echo(isset($_POST['accountNumber']) ? 
    $_POST['accountNumber'] : ''); ?>" />
<br />
<input type="submit" value="Save" name="submit" /></div>
</form>
</body>
</html>

Listing 6 shows the contents of the /tmp directory.


Listing 6. The session files in the /tmp directory
-rw-------  1 _www    wheel       97 Aug 18 20:00 sess_9e4233f2cd7cae35866cd8b61d9fa42b

As you can see, the session file, when printed (see Listing 7), contains the information in a fairly readable format. Because the file has to be readable and writable by the Web server user, the session files can create a major problem for anyone on a shared server. Someone other than you can write a script that reads these files so they can try to get values out of the session.


Listing 7. The contents of a session file
userName|s:5:"ngood";accountNumber|s:9:"123456789";

Storing passwords

Passwords should never, ever, ever be stored in plain text anywhere — not in a database, session, file system, or any other form. The best way to handle passwords is to store them encrypted and compare the encrypted passwords with one another. Although this may seem obvious, storing them in plain text seems to be done quite a bit in practice. Any time you use a Web site that can send you your password instead of resetting it means that either the password is stored in plain text or there is code available for decrypting the password if it's encrypted. Even if it's the latter, the code for decryption can be found and exploited.

You can do two things to guard your session data. The first is to encrypt anything that you put into session. But just because you've encrypted the data doesn't mean it's completely safe, so be careful with relying on this as your sole means of guarding your session. The alternative is to store your session data in a different place, like a database. You still have to make sure you're locking down your database, but this approach solves two problems: First, it puts your data into a more secure place than a shared file system; second, it enables your application to scale across multiple Web servers more easily with shared sessions across multiple hosts.

To implement your own session persistence, see the session_set_save_handler() function in PHP. With it, you can store session information in a database or implement a handler for encrypting and decrypting all of your data. Listing 8 provides an example of the function use and skeleton functions for implementation. You can also check out examples of how to use a database in the Resources section.


Listing 8. session_set_save_handler() function example
function open($save_path, $session_name)
{
    
    return (true);
}
function close()
{
    
    return (true);
}
function read($id)
{
    
    return (true);
}
function write($id, $sess_data)
{
    
    return (true);
}
function destroy($id)
{
    
    return (true);
}
function gc($maxlifetime)
{
    
    return (true);
}
session_set_save_handler("open", "close", "read", "write", "destroy", "gc");



Back to top




0

阅读 收藏 转载 喜欢 打印举报
  

新浪BLOG意见反馈留言板 电话:4006900000 提示音后按1键(按当地市话标准计费) 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

新浪公司 版权所有