Secure PHP Coding Tips – Hands On

Finding a list of what not to do when coding in PHP is easy enough, but what is often lacking are real world examples of how to remediate insecure code. Here are a few common insecure coding practices to watch out for in PHP code, and applicable ways you can fix them today.

  1. SQL Injection vulnerabilities: Be careful when using user input in SQL queries. Use prepared statements with bound parameters instead of concatenating user input with SQL statements.
  2. Cross-Site Scripting (XSS) vulnerabilities: Make sure to sanitize user input before displaying it on the website to prevent malicious scripts from running on the client’s browser.
  3. File inclusion vulnerabilities: Avoid using user-controlled input in file inclusion functions like require() or include(). Instead, use whitelists or restrict access to certain files and directories.
  4. Insecure session management: Ensure that session IDs are generated securely and are not vulnerable to session fixation attacks. Also, make sure to destroy session variables when they are no longer needed.
  5. Insecure password storage: Do not store passwords in plain text. Use secure hashing algorithms such as bcrypt to hash and salt passwords.
  6. Code injection vulnerabilities: Avoid using user-controlled input in eval() or similar functions that can execute arbitrary code.

These are just a few examples of insecure coding practices in PHP. Finding similar insecurities in your can take some coordinated planning, careful searches.

Generate Session IDs Securely to Prevent Session Fixation Attacks with PHP and Nginx

To ensure that session IDs are generated securely and are not vulnerable to session fixation attacks, you can follow these steps:

  1. Set the session cookie parameters:
session_set_cookie_params([
    'lifetime' => 0, // session cookie lifetime in seconds (0 = until browser is closed)
    'path' => '/', // path on the domain where the cookie will work
    'secure' => true, // send the cookie only over secure connections (HTTPS)
    'httponly' => true, // prevent JavaScript from accessing the session cookie
    'samesite' => 'Strict', // restricts the cookie to only be sent with "same-site" requests
]);

In this example, the session_set_cookie_params() function is used to set the session cookie parameters. The lifetime parameter is set to 0 to make the cookie a session cookie that will expire when the browser is closed. The path parameter is set to / to make the cookie available across the entire domain. The secure parameter is set to true to ensure that the cookie is only sent over secure connections (HTTPS). The httponly parameter is set to true to prevent JavaScript from accessing the session cookie. Finally, the samesite parameter is set to Strict to restrict the cookie to only be sent with “same-site” requests.

  1. Use a cryptographically secure random number generator to generate the session ID:
session_id(bin2hex(random_bytes(32))); // generate a session ID using random_bytes()

In this example, the session_id() function is used to generate the session ID using random_bytes() function, which generates a cryptographically secure random number of bytes.

  1. Regenerate the session ID periodically:
if (rand(1, 100) === 1) { // regenerate the session ID with a 1% chance
    session_regenerate_id(true); // regenerate the session ID and delete the old session data
}

In this example, the session_regenerate_id() function is used to regenerate the session ID with a 1% chance. The true parameter is passed to the function to delete the old session data.

By following these steps, you can ensure that session IDs are generated securely and are not vulnerable to session fixation attacks in your PHP application running on nginx web server.

Use “Approval Lists” (a.k.a whitelists) in PHP to Restrict Access to Specific Files and Directories

To restrict access to certain files and directories in PHP, you can use either an “approval” list (previously referred to as a “white list”) or an “exclude” (previously black list) approach. The terms have evolved, though the concepts have not. Here are some code samples for both approaches:

  1. Whitelist approach using $_GET or $_POST parameters:
// Define a whitelist of allowed pages
$allowedPages = ['home', 'about', 'contact'];

// Check if the requested page is in the whitelist
if (isset($_GET['page']) && in_array($_GET['page'], $allowedPages)) {
    // Include the requested page
    include($_GET['page'] . '.php');
} else {
    // Display an error message or redirect to a default page
    echo "Invalid page requested!";
}

In this example, a whitelist of allowed pages is defined using an array. The $_GET['page'] parameter is then checked to see if it is in the whitelist. If it is, the corresponding page is included using include(). If not, an error message is displayed or the user is redirected to a default page.

  1. Blacklist approach using .htaccess file:
// Create an .htaccess file in the directory you want to protect 
Deny from all

// Allow access to specific files or directories 
<FilesMatch "^(index\.php|css|js)"> 
    Allow from all 
</FilesMatch>

In this example, an .htaccess file is created in the directory you want to protect. The Deny from all directive prevents access to all files in the directory. The FilesMatch directive is then used to allow access to specific files or directories. In this case, access is allowed to the index.php file, as well as any files in the css and js directories.

By using either a whitelist or blacklist approach to restrict access to certain files and directories, you can help prevent unauthorized access to sensitive files and directories in your PHP application.

Preventing Cross-site Scripting by Sanitizing User Input

To sanitize user input before displaying it on a website and prevent cross-site scripting (XSS) attacks, you can use the PHP htmlspecialchars() function or the htmlentities() function. Here are a few examples using htmlspecialchars():

// Sanitize user input using htmlspecialchars() 
$username = htmlspecialchars($_POST['username'], ENT_QUOTES, 'UTF-8'); 
// Display the sanitized input on the website 
echo "Welcome, $username!";

In this example, the htmlspecialchars() function is used to sanitize the $_POST['username'] input. The second parameter (ENT_QUOTES) specifies that both single quotes and double quotes should be converted to their corresponding HTML entities. The third parameter specifies the character encoding used (in this case, UTF-8).

Similarly, here’s an example using htmlentities():

// Sanitize user input using htmlentities() 
$username = htmlentities($_POST['username'], ENT_QUOTES, 'UTF-8');
// Display the sanitized input on the website
echo "Welcome, $username!";

In this example, the htmlentities() function is used to sanitize the $_POST['username'] input. Like htmlspecialchars(), the second parameter (ENT_QUOTES) specifies that both single quotes and double quotes should be converted to their corresponding HTML entities, and the third parameter specifies the character encoding used.

By using one of these functions to sanitize user input before displaying it on a website, any potentially malicious code in the input will be converted to harmless characters and prevent XSS attacks.

Ways to Prevent SQL injections in PHP

Here are some PHP code samples demonstrating ways to prevent SQL injection attacks:

  1. Using Prepared Statements with Bound Parameters:

Prepared statements with bound parameters are the recommended approach to prevent SQL injection attacks. Here’s an example:

// Initialize a PDO instance
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'username', 'password');

// Prepare a SQL statement with placeholders
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username');

// Bind values to the placeholders
$username = $_POST['username'];
$stmt->bindParam(':username', $username);

// Execute the statement
$stmt->execute();

// Fetch the results
$results = $stmt->fetchAll();

In this example, the SQL statement contains a placeholder :username instead of directly including the user input. The user input is then bound to this placeholder using the bindParam() method.

  1. Using mysqli_real_escape_string():

While prepared statements are recommended, mysqli_real_escape_string() can also be used to prevent SQL injection attacks:

// Initialize a mysqli instance
$conn = mysqli_connect('localhost', 'username', 'password', 'mydb');

// Escape user input using mysqli_real_escape_string()
$username = mysqli_real_escape_string($conn, $_POST['username']);

// Build and execute the SQL statement
$sql = "SELECT * FROM users WHERE username = '$username'";
$result = mysqli_query($conn, $sql);

// Fetch the results
$results = mysqli_fetch_all($result, MYSQLI_ASSOC);

In this example, the user input is escaped using the mysqli_real_escape_string() function before including it in the SQL statement.

It’s important to note that while escaping input can help prevent SQL injection attacks, it’s not a foolproof solution and can still leave your application vulnerable. Prepared statements with bound parameters are the recommended approach.

Errors installing Laminas MVC Skeleton

If you’re trying to start the basic Laminas skeleton application and the very first step of installation is failing, it would be fair to assume the project is going to be a mess moving forward. The good news is that moving past the PHP critical error is pretty easy.

PHP 7.4x, Composer 2.

The problem is during the initial step of installing the MVC Laminas framework using Composer 2 with PHP 7.4+.

composer create-project -s dev laminas/laminas-mvc-skeleton path/to/install

Generates the following error, which then produces a stack trace implicating OptionalPackagesInstaller.php as the culprit.:

PHP Fatal error: Uncaught TypeError: Argument 1 passed to Symfony\Component\Console\Question\Question::__construct() must be of the type string, array given, called in /usr/share/php/Composer/IO/ConsoleIO.php on line 273 and defined in /usr/share/php/Symfony/Component/Console/Question/Question.php:38

Fear not! The fix is surprisingly easy. You’ll be editing the following file in your project folder:

vendor/laminas/laminas-skeleton-installer/src/OptionalPackagesInstaller.php and correcting one mistake, the passing of an array instead of a string, in 2 different places:

  1. OptionalPackagesInstaller.php, Line 138: remove the opening bracket following the ‘=’ when setting the $question variable. Then, remove the closing bracket and the comma (around line 143).
  2. Same file, same concept: remove the brackets and unnecessary comma from the $question variable set in line 169.

Re-run composer install after saving the corrected file. It should now prompt you for the installation options.

If it throws any issues regarding the installation of PHP modules, review how you installed PHP initially and address each issue accordingly.