Mass XSSodus in PHP

Published: 2014-03-27
Last Updated: 2014-03-27 16:14:10 UTC
by Alex Stanford (Version: 1)
4 comment(s)

In writing web applications PHP developers often find themselves repeatedly calling the htmlentities function, or the htmlspecialchars function. These will encode the special characters of a string to their HTML entities, ensuring that output can safely avoid being executed by browser parsing engines.

The problem with this is a human one. Humans do make mistakes, and even those well aware of the consequences and solutions will eventually suffer from an oversight that results in an XSS vulnerability. How can we limit the possibility of creating vulnerabilities in such a situation?

We’ve seen a very fair share of approaches to mitigating XSS in PHP but one in particular seems to fly under the radar. PHP has a couple of configuration directives in php.ini which will automagically filter input by various sanitization and/or validation flags of your choosing. So, can we make it work like htmlentities? Yes!

In your (recent) default php.ini file you will find the following:

	[filter]
	; http://php.net/filter.default
	;filter.default = unsafe_raw
	
	; http://php.net/filter.default-flags
	;filter.default_flags =
	

Modify these as follows:

	[filter]
	; http://php.net/filter.default
	filter.default = full_special_chars
	
	; http://php.net/filter.default-flags
	filter.default_flags = 0
	

This will encode all $_GET, $_POST, $_COOKIE, $_REQUEST and $_SERVER values. (The original data can be accessed through the filter_input() function.)

Example In Action

As a quick proof of concept I built a simple login form that does no sanitization or encoding. The first (Username) field is pre-filled with the data you submitted if an error occurs, such as not providing any password. To exploit the first form field, I entered "><script>alert("XSS");</script> with no password at all.

Without the php.ini configuration changes:

The input data is parsed by the browser as code and the JavaScript alert is displayed, thereby proving the presence of an XSS vulnerability.

Now, with the php.ini configuration changes:

The input data is safely output into the form field as content instead of code, thereby mitigating the XSS vulnerability.

Common Questions

  • Do I still need to perform output encoding in my application?
    Yes. This approach will handle a large portion of the repetitive cases, but some necessity for output encoding will remain. A simple example would be the importance of using the urlencode function upon outputting a URL which contains user input.
  • What about JSON/JavaScript output?
    Any input you place into JSON or JavaScript from PHP’s superglobals would still be encoded.
  • Does this work for distributable web apps that run in shared hosting environments?
    This approach may not always be feasible in shared environments due to the potentially limited access to php.ini directives. If you’re building distributable web apps which support running in shared environments, it is not safe to rely on this approach. However, if you’re working in an environment with a custom PHP back-end running on a dedicated server(s) this approach may be your best bet.
  • Won’t this result in double encoding?
    Yes, quite possibly. That said, double encoding is far lower risk and more easily identifiable than XSS vulnerabilities.
  • How can I check that the directives are properly set before outputting anything from my application?
    The following code will check that the php.ini settings are in place as expected and discontinue execution with a relevant error otherwise. It should be placed at the beginning of your application before any other code is executed.
    if(ini_get('filter.default')!=='full_special_chars'||ini_get('filter.default_flags')!=='0') 
        die('Missing and/or incorrect filter.default and/or filter.default_flags directives in php.ini');
    

Not a Replacement for Defense in Depth

While this approach can simplify output encoding and limit the risk of developer oversight, it should not be considered an end-all solution. You may have input data sources in your application other than PHP's superglobals. You should still consider the results of a SQL query or cURL request, for example, as potentially malicious. Finally, you should continue performing penetration testing and code reviews to catch that inevitable XSS vulnerability before they do.

Keywords: php xss
4 comment(s)

Comments

Something's gone pear-shaped with the email notification, because I have received 7 emails for this diary entry.
[quote=comment#30159]Something's gone pear-shaped with the email notification, because I have received 7 emails for this diary entry.[/quote]
No doubt about it. Something went wrong in our notifications back-end. I've temporarily resolved the issue by suspending notifications as a whole while we seek out the underlying cause and a permanent solution. Sincerest apologies!
I like this in principal, BUT disaster waiting to happen, if the PHP.ini gets reverted or "tweaked".

Perhaps it makes sense to not use PHP superglobals within a program at all.
Do the sanitization of inputs yourself in an automated way, when capturing the input from the superglobals,
to application-specific variables.

Because the system or local PHP.ini is totally separate from the program.
It can be reverted if users don't follow directions, OR it's incompatible with other scripts
the server must run; various scenarios can make php.ini customization go away.

Therefore; it's not safe to rely on within the context of an application:
it's similar to the magic_quotes idea.

Do you have a suggested way to establish this AND couple this setting more tightly with the PHP application?
Without having to rely on the user to make sure that PHP.ini has customizations, AND these customizations are working?

If not... your application's security correctness (including: safety of inputs) should not be reliant on a PHP.INI setting!
[quote=comment#30163]
Perhaps it makes sense to not use PHP superglobals within a program at all.
Do the sanitization of inputs yourself in an automated way, when capturing the input from the superglobals,
to application-specific variables.
[/quote]

Writing custom sanitization methods which account for the context is not a bad approach, and was my preferred approach prior discovering this one. However, I think that approach is still more likely to result in developer mistakes than relying on the core of PHP, a broadly reviewed codebase, to handle the task with only 2 directives. (and a couple lines of code to check that the directives are in place, as you pointed out and I will discuss next.) Not only more likely to result in developer mistakes when writing the custom sanitization methods, but also the issue of remembering to use those methods instead of superglobals in every case. (Including the case where it's an emergency hotfix and the pressure is on) Additionally, custom sanitization methods add extra learning curve overhead when adding a new PHP developer to the project as compared to utilizing superglobals which every PHP developer would be aware of.

[quote=comment#30163]
Because the system or local PHP.ini is totally separate from the program.
It can be reverted if users don't follow directions, OR it's incompatible with other scripts
the server must run; various scenarios can make php.ini customization go away.

Therefore; it's not safe to rely on within the context of an application:
it's similar to the magic_quotes idea.

Do you have a suggested way to establish this AND couple this setting more tightly with the PHP application?
Without having to rely on the user to make sure that PHP.ini has customizations, AND these customizations are working?[/quote]

Without having tested it (yet) I believe you could couple the application with the ini settings by utilizing the PHP core function ini_get to check whether the settings are in place as expected. Short of that, you could run die('Incorrect php.ini settings'); etc.

I wrote that this may not be a good solution in distributed applications (especially which will run in shared environments), but is better suited to an environment where it's a custom web application running on a dedicated server. (So, I would not recommend this approach for say, Wordpress or WHMCS)

Admittedly, you've made a very good point about coupling the application with the ini settings. I am going to verify my suspicion that ini_get will provide a solution and add it to the diary if so. Thank you very much for the feedback!

P.S. I was never a fan of magic quotes. :)

Update: I've verified that ini_get is a working solution and I've added it to the "Common Questions" section of this diary. For those who don't wish to scroll back up, you would add this code to the beginning of your application:

if(ini_get('filter.default')!=='full_special_chars'||ini_get('filter.default_flags')!=='0')
die('Missing and/or incorrect filter.default and/or filter.default_flags directives in php.ini');

Diary Archives