PHP $_SERVER variables are not safe for use in forms, links

A common security mistake I see WordPress plugin authors (and PHP coders in general) make is using $_SERVER['PHP_SELF'] or $_SERVER['REQUEST_URI'] as the action of a form or part of an anchor’s href attribute. This is not safe to do, and opens your code up to XSS (cross-site scripting) exploits.

Common example:

<form action="<?php echo $_SERVER['PHP_SELF']; ?>">

Another example:

<a href="<?php echo $_SERVER['PHP_SELF']' ?>?foo=bar">link title</a>

Here are my two rules regarding $_SERVER['PHP_SELF'] or $_SERVER['REQUEST_URI'] in forms:

  • Do not use them
  • If you use one of them, escape it with esc_url()

Most uses of $_SERVER['PHP_SELF'] and $_SERVER['REQUEST_URI'] are in HTML forms. If you want the action attribute to point to the current URL, leave it blank. URI references that are blank point to the current resource.

<form action="">

If you do want to specify the action (and there are good reasons for wanting to do that, such as stripping the query string from the current URL), you must run it through esc_url().

<form action="<?php echo esc_url( $_SERVER['PHP_SELF'] ); ?>">

The same applies to links… run the href attribute through esc_url().

<a href="<?php echo esc_url( $_SERVER['PHP_SELF'] . '?foo=bar' ); ?>">link title</a>

A quick search through the WordPress Plugin Directory showed that this problem is far too common.

Updates:

Examples of URLs that could exploit this for double-quoted actions:

script.php/"%20onmouseover='alert(document.cookie)'

And single-quoted actions:

script.php/'%20onmouseover='alert(document.cookie)'

No, just using a plain old htmlentities() wrapper is not going to help! That’s still vulnerable to XSS in certain situations. If you’re not using WordPress, you should copy the WordPress escaping functions (just remove the apply_filters() portions).

If you are using the base tag, Safari will apply that base to the blank action attribute. So if you use the base tag (I never do), a blank action isn’t going to be for you. Use what you’ve been using, but escape it.

Lester Chan has a handy snippet for the form action of WordPress plugin settings pages:

<form action="<?php echo admin_url( 'admin.php?page=' . plugin_basename( __FILE__ ) ); ?>">

admin_url() takes care of escaping for you, and is an easy way to create a full WP admin URL from a wp-admin-relative URL.

95 thoughts on “PHP $_SERVER variables are not safe for use in forms, links

  1. What would an example of XSS attack be when using this? I’ve always run $_SERVER[‘PHP_SELF’] through htmlspecialchars. I’ve never heard of esc_url before either.

  2. A naked htmlspecialchars() won’t protect you completely.

    Consider a form with htmlspecialchars($_SERVER['PHP_SELF']) as the action, enclosed with single quotes. This will defeat it:

    script.php/'%20onmouseover='alert(document.cookie)'

    For a double-quoted version using naked $_SERVER['PHP_SELF'], this will defeat it:

    script.php/"%20onmouseover='alert(document.cookie)'

    For more on esc_url(), esc_attr() and the escaping API updates for WP 2.8, read this post.

  3. And I’ve done this hundreds and hundreds of time. It was actually in the O’Reilly PHP 4 book that I learned from. 😦

  4. Hi Mark,

    How does <form action="”> leave you open for XSS? I’ve never heard of this exploit before, do you have a link with more information?

    I typically do this form actions, because there’s an issue with Safari. If you supplied a earlier in the document, the form action goes there instead. (Last I checked, anyhow)

  5. Thanks for writing this post! This knowledge really needs to be spread around. 🙂

    Question though: are the other $_SERVER variables safe? I thought I had read somewhere it was possible for a user to modify $_SERVER[‘HTTP_HOST’]. I could be remembering entirely wrongly though.

  6. How does <form action=""> leave you open for XSS? I’ve never heard of this exploit before, do you have a link with more information?

    I didn’t say that it did… was there something specifically that seemed ambiguous on that point? Let me know and I’ll update to make it more clear.

  7. Question though: are the other $_SERVER variables safe? I thought I had read somewhere it was possible for a user to modify $_SERVER['HTTP_HOST']

    I wouldn’t consider any of them safe. Any info that you didn’t specifically and statically populate yourself should be considered unsafe.

    1. But of course that depends on how and where they’re used, right? We briefly discussed at WCPDX the use of $_SERVER['HTTP_HOST'] in the WP_SITEURL and WP_HOME constants to make wp-config.php more portable between dev and production systems. In this use case, the web server is doing the filtering for you, in that any hostname it doesn’t recognize will never hit the VirtualHost in question (assuming a properly configured web server).

    2. Not all servers may be set up correctly, and may serve for *:80. When coding a plugin for public consumption, I wouldn’t assume they haven’t done that.

      For your example of using WP_SITEURL to allow for portable dev/production systems, you could whitelist certain domains (localhost/yourblog.com) or at least pass the HTTP_HOST setting through a [a-z0-9.-] filter.

      You’re right about context, but when dynamically populating a powerful constant like WP_SITEURL, it might be prudent to filter out unexpected data. That’s good coding practice anyway — especially in a dynamically-typed language like PHP where the difference between 0 and FALSE could cause your code to break.

    3. Question though: are the other $_SERVER variables safe? I thought I had read somewhere it was possible for a user to modify $_SERVER[‘HTTP_HOST’]. I could be remembering entirely wrongly though.

  8. How about this one:

    <?php
    $urlpath = explode("/", $_SERVER['REDIRECT_URL']);
    $type_in_url = $urlpath[2];

    Is it safe you reckon? I use that in two non-WordPress files.

  9. Sorry folks, comment form took out the PHP tag, and I didn’t double check. My bad.

    I typically do . I do this because in Safari, if you have a and then do , the action defaults to your BASE URL, instead of the current URL.

    Do you have a link with more information on how users can modify the $_SERVER variables?

  10. Retry:

    I typically do:

    <form action="<?php echo $_SERVER['REQUEST_URI'] ?>">

    I do this because in Safari, if you have a base href="#" then have form action="", the action defaults to your base URL, not the current resource.

    1. That’s a good point. You can use REQUEST_URI, but you need to filter it. In WP, esc_url() is your ticket. In non-WP… copy WP’s security functions and use them. 🙂

  11. @Anne: Again, I wouldn’t trust anything you didn’t explicitly and statically set yourself. If there is any amount of unpredictable variability, it’s suspect.

    This is mostly a matter of good habits. Once you get in them, it’ll come naturally, and it won’t slow you down.

    1. Mark, thanks, I will search for how would be the best to change that bit of the code for something else instead. Previously it was (EmptyReferrence!) also in form url. I am not programmer but, guys like you make looking for a related stuff easier. Thank you Mate.

  12. So how exactly is that a security vulnerability?… I could claim anything is a vulnerability. Some examples would be really great otherwise this post is worthless to us and I leave not learning anything.

    1. It allows for script injection — the running of arbitrary Javascript code. See the second comment on this post. The precise method of leveraging XSS is executing cookie-stealing Javascript in an authenticated user’s browser. With this access to an administrator-level account, an attacker can gain deeper levels of access.

      My goal with this post wasn’t to teach XSS from scratch, but bring to light a common XSS-enabling mistake that I’ve seen even XSS-aware coders make.

    1. I have a local checkout of all the plugins in the repository, so I used fgrep. Unfortunately I don’t know of a hosted way to search through all the plugins…

  13. Interesting catch – there are a ton of books out there that use this method to teach you how to get up and running with form handling quickly. I’ve been doing it this way for over 6 years – except I never used it in production scenarios.

  14. Hi,

    come on guys, don’t go bashing on those book authors from a few years ago. They were just trying to teach programming in a general way, not for security.

    Although Mark is of course right about the dangers of XSS and the need to properly escape things, I don’t really see this problem with PHP_SELF. It is set by the server and if the server can be manipulated to have PHP_SELF contain a string like in Mark’s example, there are probably other security issues with that server.
    The problem is more severe with HTTP_USER_AGENT though.

    Regards,
    Tobias

    1. @Tobias, This isn’t an issue only with some esoteric server setup. Stock Apache 2.0 with stock PHP 5.2 on Linux is vulnerable to PHP_SELF injection like the following:

      script.php/"%20onmouseover='alert(document.cookie)'

  15. Hi Mark, thanks for the nice article. I have a question:

    I use action="/". Do you think this is safe in your opinion?

  16. Oops, the previous comment got eaten away by the stripper:

    I use bloginfo(‘home’) template tag for action; do you think this is safe?

  17. Classic attack, its nearly always safer to hard code the URL. Its surprising the number of websites that are still vulnerable to XSS and CSRF attacks today.

  18. I just headed over here to read this article from a WordPress Dashboard…The title caught my attention big time because I work for a National Company and about a month ago during a routine security probe by controlscan-com, the only report that came back was this Javascript XSS expolit by my use of having Server Variables un-escaped.

    Great read, an a little extra work for me this morning wordpress and non-wordpress –

  19. Awesome tip! I didn’t even know that could be an issue. I had seen the $_SERVER[‘PHP_SELF’] trick, and have used it ever since. Appreciate the improved version! Thanks. 🙂

  20. It’s good your talking about this exploit, but you don’t really explain how it would be used very well. Here’s a better description:

    http://bit.ly/qWUK4

    This is how an attack could happen…

    1. Fred, who is a member of your site, gets an email from a scammer saying their account has been compromised.
    2. In the message, the scammer asks the user to confirm their account using a link (to your website).
    3. That link includes extra JavaScript code added to the end of the URL that sends whatever is entered into the form to the scammer’s website
    4. Fred follows the link and logs in. His account looks fine, but he’s just inadvertently given away his account login.

    1. Hey Cody! your the only person i actually know who has explained this perfectly. OMG i have read a ton of articles on this and still did not understand it. Now i do. I am wondering which is the best way to protect myself from this, i’m not sure.

    1. It may be, but why not escape, just in case? Consider this to be my version of Blaise Pascal’s Wager: What do you have to lose by believing that $_SERVER values are all suspect? 🙂

  21. Well said re: Pascal’s Wager. I would rather add a tiny bit of overhead to insure that I still own my data rather than find out the hard way.

    It’s bad enough when it’s only your internal code, but the vulnerability footprint is huge if it’s popular. Consider WP itself, in self-hosted terms.

    Any vulnerability can crop up, and it may take a day or two for even their dedicated folks to fix and post a new incremental build. I believe in good hygiene as to login attempts, filtering the admin directory via other methods, etc. in addition to the default.

    Appreciate the reminder.

  22. This is how I currently have it set up for a form I’m developing for a client. The form returns to itself, and instead of leaving the action blank, I thought to set it to the permalink of the page the form is on.

    <?php echo esc_url( get_permalink($post->ID) . '?action=post-new' );?>
    

    But I’m wondering since it’s using an internal WP function, is escaping still necessary?

    1. HTTP_HOST doesn’t suffer from the same issues as PHP_SELF and REQUEST_URI because it only holds the host part of the URL which can’t be modified by the user (or it wouldn’t be processed by your server).

  23. 今天百度挂了,呵呵,流量减低了一大半,真是的。郁闷之余来看看博主的博客,写的不错,加油。

  24. I’ve been trying to figure out how this is a security vulnerability for months. But a lightbulb just went off while looking through a few other links on the subject. That’s a nasty little trap to fall into! Thanks for the heads up. I don’t think I’ve used it before, but I can see how I could have used it without blinking an eye at it if I hadn’t read this blog post (numerous times).

  25. @Mark – You recently found this security issue in a plugin in the repository. I won’t name it since it hasn’t been fixed yet, but I’m confused as to how this hack could affect that plugin since the form was something that is only displayed in the admin page. So wouldn’t that just mean that the only person who could hack the site, is the site owner? …. which really doesn’t seem like a security vulnerability.

    1. This was a stupid question. Clearly the answer was that someone simply had to send a link to the admin page to the site owner. Whammo, instant XSS attack. Now that I’ve learned a lot more about this sort of thing it’s scary to see the number of plugins that still do this.

  26. Thanks so much for this post, v. valuable info.
    I’m somewhere between noob and intermediate with php, and have been using $_SERVER[‘PHP_SELF’] for form processing for quite some time now. I’ll go fix.

    Quick Q, not too off topic I hope – is it safe to use $_SERVER[‘DOCUMENT_ROOT’] within an include call? I use this method all the time for pulling in header, nav etc.

  27. In a more general context, I’ve always been torn between the “filter what you use” and “pre-filter” arguments (for certain types of data).

    I think a valid argument could be made for at least running the $_SERVER and $_GET arrays through a filtering function before any other code is executed (re-building $_REQUEST afterwards). The data they hold shouldn’t be complex enough to get mangled by an xss filter IMO.

    So many times you see examples like:

    header("Location: " . $_GET['redirect']);
    echo $_SERVER['HTTP_USER_AGENT'];

    Of course you should still filter what you use as well, but if others are writing plugins then it can make a difference.

    My own benchmarking has shown that to pre-filter every argument in the $_SERVER array with quite a thorough function (decoding data, removing unsafe strings such as data: and htmlspecialchars) takes only a few thousandths of a second, so it would seem to be worth considering.

  28. Nice info, i’ve never really considered main server vars as a potential injection source.

    Be nice to have a list of the server created vars for reference but always best to escape everything anyway. I used to use htmlentities myself as well but my favourite escaping method now is using ctype i like the way you can set what characters are allowed

    $badString = 'file.php\\"?&%^';
    $goodString = 'file.php?id=37&item=3';
    
    function checkStr($str){
    	$chars = array('.','?','&','=');//allowed characters
    	$strTest = str_replace($chars, '', $str);//take out the allowed chars to test
    	$result = ctype_alnum($strTest);//test if the string is no only alphanumeric chars
    	return $result;
    }
    if(checkStr($badString)){
      echo 'Bad string passed<br>';
    }else{
      echo 'Bad string failed<br>';
    }
    if(checkStr($goodString)){
      echo 'Good string passed<br>';
    }else{
      echo 'Good string failed<br>';
    }
    

    or if it is just quotes you don’t want you can just str_replace out the quotes or use strpos to check if it contains any quotes

    full documentation on ctype is on php.net http://php.net/manual/en/book.ctype.php as well as all the others

  29. Why would you want to protect against this?

    Anyone with Chrome can just use the developer console to alert the cookies or tamper with the form action. It really doesn’t matter. They just break the form for themselves. Anything client side can be altered together with all of the $_SERVER[‘HTTP_xxx’]; variables.

    That and quite some more stuff.

    Als long as you don’t rely on it server side, it doesn’t matter.

  30. I saw this in the code I have some students working on. I asked them where they learned to do it. They showed me a “model PHP form” from their lecture notes, outlining the optimal way to handle forms … it included an unescaped PHP_SELF in the form 😦

  31. Today needed to put together the URL of the page using those server variables and ehre’s what I did:
    echo ‘http://”.$_SERVER[‘HTTP_HOST’] . $_SERVER[‘REQUEST_URI’]);

    Too bad PHP does not have a built-in one. Or does it?

  32. Awesome post, I never even though of leaving it blank. I think I’ll do that form now on. I have been trying to figure out how to pass the current page to a class I’m working on, but this makes life a lot easier.

Comments are closed.