DEV Community

Manuel Canga
Manuel Canga

Posted on

That Strange PHP Code in Frameworks and CMSs

Desarrollo web

Note: To follow along with this post, it's assumed you have some basic knowledge of programming in PHP.

This article discusses a PHP code snippet that you’ve likely seen at the top of your favorite CMS or framework. You've probably read that you should always include it at the beginning of every PHP file you develop, for security reasons, although without a very clear explanation of why. I’m referring to this code:

<?php

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly
}
Enter fullscreen mode Exit fullscreen mode

This type of code is very common in WordPress files, although it appears in nearly all frameworks and CMSs. In the case of the Joomla CMS, for example, the only change is that instead of ABSPATH, it uses JEXEC. Other than that, the logic remains the same. This CMS evolved from a previous system called Mambo, which also used similar code, but with _VALID_MOS as the constant. If we go even further back, we find that the first CMS to use this kind of code was PHP-Nuke (considered by some to be the first PHP-based CMS).

The execution flow of PHP-Nuke (and most CMSs and frameworks today) consisted of sequentially loading multiple files to respond to the action the user or visitor had taken on the website. For example, imagine a website from that era hosted at example.net with this CMS installed. Every time the homepage loaded, the system executed a sequence of files in order (this is just an example, not an actual sequence): index.php => load_modules.php => modules.php. In this chain, index.php was loaded first, which then loaded load_modules.php, which in turn loaded modules.php.

This execution chain didn’t always start with the first file (index.php). In fact, anyone could bypass part of the flow by directly accessing one of the other PHP files through its URL (for example, http://example.net/load_modules.php or http://example.net/modules.php), which, as we will see, could be risky in many cases.

How was this problem solved? A security measure was introduced, adding code similar to this at the beginning of each file:

<?php

if (!eregi("modules.php", $HTTP_SERVER_VARS['PHP_SELF'])) {
    die ("You can't access this file directly...");
}
Enter fullscreen mode Exit fullscreen mode

Essentially, this code, placed at the top of a file named modules.php, checked if modules.php was being accessed directly via the URL. If so, execution was stopped, displaying the message: “You can’t access this file directly…” If $HTTP_SERVER_VARS['PHP_SELF'] didn’t contain modules.php, it meant that the normal execution flow was active, allowing the script to continue.

This code, however, had some limitations. First, the code was different for each file in which it was inserted, which added complexity. Additionally, in certain situations, PHP did not assign a value to $HTTP_SERVER_VARS['PHP_SELF'], which limited its effectiveness.

So, what did the developers do? They replaced all those code snippets with a simpler and more efficient version:

<?php

if (!defined('MODULE_FILE')) {
    die ("You can't access this file directly...");
}
Enter fullscreen mode Exit fullscreen mode

In this new code, which had become quite popular in the PHP community, the existence of a constant was checked. This constant was defined and assigned a value in the first file of the execution flow (index.php, home.php, or a similar file). Therefore, if this constant didn’t exist in any other file in the sequence, it meant that someone had bypassed index.php and was attempting to access another file directly.

Dangers of Directly Running a PHP File

At this point, you may be thinking that breaking the execution chain must be extremely serious. However, the truth is that, usually, it doesn’t pose a major threat.

The risk might arise when a PHP error exposes the path to our files. This shouldn’t concern us if the server is configured to suppress errors; even if errors weren’t hidden, the exposed information would be minimal, providing only a few clues to a potential attacker.

It could also happen that someone accesses files containing HTML fragments (views), revealing part of their content. In most cases, this should not be a cause for concern either.

Finally, a developer, either by mistake or lack of experience, might place risky code without external dependencies in the middle of an execution flow. This is very uncommon since framework or CMS code generally depends on other classes, functions, or external variables for its execution. So, if an attempt is made to execute a script directly through the URL, errors will arise as these dependencies won’t be found, and the execution won’t proceed.

So, why add the constant code if there is little reason for concern? The answer is this: "This method also prevents accidental variable injection through a register globals attack, preventing the PHP file from assuming it's within the application when it’s actually not."

Register Globals

Since the early days of PHP, all variables sent via URLs (GET) or forms (POST) were automatically converted into global variables. For example, if the file download.php?filepath=/etc/passwd was accessed, in the download.php file (and in those depending on it in the execution flow), you could use echo $filepath; and it would output /etc/passwd.

Inside download.php, there was no way to know if the variable $filepath was created by a prior file in the execution chain or if it was tampered with via the URL or POST. This created significant security vulnerabilities. Let’s look at an example, assuming the download.php file contains the following code:

<?php

if(file_exists($filepath)) {
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="'.basename($filepath).'"');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($filepath));
    flush(); // Flush system output buffer
    readfile($filepath);
    exit;
}
Enter fullscreen mode Exit fullscreen mode

The developer likely intended to use a Front Controller pattern for their code, meaning all web requests would go through a single entry file (index.php, home.php, etc.). This file would handle session initialization, load common variables, and finally redirect the request to a specific script (in this case, download.php) to perform the file download.

However, an attacker could bypass the intended execution sequence simply by calling download.php?filepath=/etc/passwd, as mentioned before. PHP would automatically create the global variable $filepath with the value /etc/passwd, allowing the attacker to download that file from the system. Serious problem.

This is only the tip of the iceberg since even more dangerous attacks could be executed with minimal effort. For example, in code like the following, which the programmer might have left as an unfinished script:

<?php

require_once($base_path."/My.class.php");
Enter fullscreen mode Exit fullscreen mode

An attacker could execute any code by using a Remote File Inclusion (RFI) attack. If the attacker created a file My.class.php on their own site https://mysite.net containing any code they wanted to execute, they could call the vulnerable script by passing in their domain: useless_code.php?base_path=https://mysite.net, and the attack would be complete.

Another example: in a script named remove_file.inc.php with the following code:

<?php

if(file_exists($filename)) {
    if( unlink($filename) ) {
        echo "File deleted";
    }
}
Enter fullscreen mode Exit fullscreen mode

an attacker could call this file directly with a URL like remove_file.inc.php?filename=/etc/hosts, attempting to delete the /etc/hosts file from the system (if the system allows it, or other files they have permission to delete).

In a CMS like WordPress, which also uses global variables internally, these types of attacks were devastating. However, thanks to the constant technique, these and other PHP scripts were protected. Let’s look at the last example:

<?php

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly
}

if(file_exists($filename)) {
    if( unlink($filename) ) {
        echo "File deleted";
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, if someone attempted to access remove_file.inc.php?filename=/etc/hosts, the constant would block the access. It is essential that this is a constant because, logically, if it were a variable, an attacker could inject it.

By now, you may wonder why PHP kept this functionality if it was so dangerous. Also, if you know other scripting languages (JSP, Ruby, etc.), you’ll see they have nothing similar (which is why they also don’t use the constant technique). Recall that PHP was initially created as a C-based templating system, and this behavior made development easier. The good news is that, seeing the issues it caused, PHP maintainers introduced a php.ini directive called register_globals (enabled by default) to allow this functionality to be disabled.

But as problems persisted, they disabled it by default. Even so, many hosts kept enabling it out of fear that their clients’ projects would stop working, as much of the code at the time did not use the recommended HTTP_*_VARS variables to access GET/POST/... values but rather used global variables.

Finally, seeing that the situation didn’t improve, they made a drastic decision: to remove this functionality in PHP 5.4 to avoid all these problems. Thus, today, scripts like those we’ve seen (without using constants) are usually no longer a risk, except for some harmless warnings/notices in certain cases.

Current Use

Today, the constant technique is still common. However, the unfortunate reality — and the reason for this article — is that few developers understand the true reason behind its use.

As with other best practices from the past (like copying parameters into local variables inside functions to avoid issues with references or using underscores in private variables to distinguish them), many continue to apply it simply because someone once told them it was a good practice, without questioning whether it still adds value today. The truth is that, in the majority of cases, this technique is no longer necessary.

Here are some reasons why this practice has lost relevance:

  • Removal of *register globals: Since PHP 5.4, the automatic registration of GET and POST variables as globals in PHP was removed. Without *register globals, executing individual scripts directly is harmless, removing the main reason for this technique.

  • Better Code Design: Even in pre-PHP 5.4 versions, modern code is better structured, generally in classes and functions, making access or manipulation via external variables more challenging. Even WordPress, which traditionally used global variables, minimizes these risks.

  • Use of *front-controllers: Nowadays, most web applications employ well-designed *front-controllers to ensure that class and function code only executes if the execution chain starts at the main entry point. Thus, if someone attempts to load files in isolation, the logic won’t trigger unless the flow starts from the correct entry point.

  • Class Autoloading: With the widespread use of class autoloading in modern development, the use of include or require has significantly decreased. This reduces risks associated with these methods (like Remote File Inclusion or Local File Inclusion) among more experienced developers.

  • Separation of Public and Private Code: In many modern CMSs and frameworks, public code (like assets) is separated from private code (logic). This measure is especially valuable, as it ensures that, if PHP fails on the server, PHP code (whether or not it uses the constant technique) is not exposed. Although this separation wasn’t implemented specifically to mitigate register globals, it helps prevent other security issues.

  • Widespread Use of Friendly URLs: Nowadays, configuring servers to use friendly URLs is common practice, ensuring a single entry point for application logic. This makes it nearly impossible for anyone to load PHP files in isolation.

  • Error Suppression in Production: Most modern CMSs and frameworks disable error output by default, so attackers don’t find clues about the application’s inner workings, which could facilitate other types of attacks.

Even though this technique is no longer necessary in the majority of cases, that doesn’t mean it’s never useful. As a professional developer, it’s essential to analyze each situation and decide whether the constant technique is relevant to the specific context in which you’re working. This kind of critical thinking should always be applied, even to so-called best practices.

Not Sure? Here are Some Tips

If you’re still unsure when to apply the constant technique, these recommendations may guide you:

  • Always use it if you think your code might run on a PHP version earlier than 5.4.
  • Don’t use it if the file only contains a class definition.
  • Don’t use it if the file contains only functions.
  • Don’t use it if the file only includes HTML/CSS, unless the HTML reveals sensitive information.
  • Don’t use it if the file only contains constants.

For everything else, if you’re in doubt, apply it. In most cases, it won’t be harmful and could protect you in unexpected circumstances, especially if you’re starting out. With time and experience, you’ll be able to assess when to apply this and other techniques more effectively.

Desarrollo PHP

Keep Learning...

Top comments (1)

Collapse
 
waynetyler profile image
WayneTyler

This is a fantastic deep dive into the origins and purpose of these security snippets in PHP-based CMSs! It's cool to see how something so simple plays such a critical role in securing execution flows. Out of curiosity, do you think modern frameworks handle these kinds of risks better inherently, or is there still a need for manual checks like this in certain cases?