3
\$\begingroup\$

I coded a php chat that stores the text in a txt file and retrieves it, and I want to know if XSS can get past it or if someone can replace xhr.open("POST","uhh.php"); with something that contains the same php code but without htmlspecialchars or something like that. What are the vulnerabilities? (More specifically, I don't want people putting html elements through the chat, and that also leads to security issues.)

This is uhh.php:

<?php
$usr=$_POST["usr"];
$msg=$_POST["msg"];
if($usr&&$msg){
    $txt="$usr: $msg";
    $txt=htmlspecialchars(join("\n",str_split($txt,33)));
    $split=explode("\n",file_get_contents("uhh.txt") . $txt);
    while(count($split)>10){
        $split=array_slice($split,1);
    }
    file_put_contents("uhh.txt",join("\n",$split) . "\n");
}
?>
watchu lookeen at???

And this is the code to the html page (JavaScript included):

<!DOCTYPE html>
<html><head>
<title>Chatting</title>
<style>
#chatlog {
text-align:left;
border:1px solid black;
width:300px;
height:200px;
}
#chatbar {
width:300px;
}
</style>
<style type="text/css"></style></head>
<body>
<center>
<div id="chatlog">Bagelhead: lol<br>Bagelhead: yes t is<br>Abe: ok proxy off now<br>Bagelhead: gtg<br>Bagelhead: :P<br>Test: Test<br>Abe: &lt;p&gt;Test&lt;/p&gt;<br>Abe: Test<br>TEST: ab<br>TEST: abe<br></div>
<input maxlength="9001" placeholder="Press / to chat" type="text" id="chatbar">
</center>
<script>
setInterval(function(){
var xhr=new XMLHttpRequest();
xhr.open("GET","uhh.txt?a="+Math.random());
xhr.onload=function(){
document.getElementById("chatlog").innerHTML=xhr.responseText.replace(/\n/g,"<br/>");
}
xhr.send();
},1000)
var cb=document.querySelector("#chatbar"),usr="";
function chat(m){
var xhr=new XMLHttpRequest();
xhr.open("POST","uhh.php");
var fd=new FormData();
fd.append("usr",usr);
fd.append("msg",m);
xhr.send(fd);
}
window.onkeyup=function(e){
if(e.keyIdentifier=="U+00BF"){
cb.focus();
}else if(e.keyIdentifier=="Enter"){
if(usr){
chat(cb.value);
cb.value="";
}else{
usr=cb.value;
cb.placeholder="Press / to chat";
cb.value="";
cb.maxLength=9001;
}
}
}
</script>

</body></html>

uhh.txt is just a blank text file.

\$\endgroup\$
3
\$\begingroup\$

Looks safe to me, but I would change the way you request the server. This way, you send many requests to the server and for more then few people using it, the server could be flooded by these requests. It would however require switching txt file for script file. Then you could use so called long poll.

You are probably wondering what long poll is and how it works. Long poll is technique where client opens connection to the server and leaves it open. When new message comes, server sends it and closes the connection. Trick is, client immediately opens new connection. Same happens when old connection times out.

Problem here is, when you use PHP on server side, it just executes some code and closes the connection. So, you have 2 possibilities: Best idea is to use server written in node.js, which is made for such things (and there are other advantages like that you don't have to store the message anywhere, you just recieve it and immediately send it to all clients and other...). If you want or have to use PHP, then you must use the sleep() function. Then, you have while loop, that each for example 200 miliseconds check for new message in database and either sends it or sleeps for another 200 miliseconds.

If you want to try, here is part of my client code which takes care of server communication:

//function, that returns right kind of AJAX object based on browser
//I don't have worst case scenario (AJAX not supported) here, because I usually check for required support at the beginning of the script
function createAjax() {
    if (window.XMLHttpRequest) {
        return new XMLHttpRequest();
    } else {
        return new ActiveXObject('Microsoft.XMLHTTP');
    }
}

/*
Class that takes care of receiving new messages from server
PARAMS:
    longPollServerURL - string - URL of server file, which acts as long poll server
    lastIdGetURL - string - URL of txt file, where last message id is saved
    newMessageCallback - function - callback to get called when new message is received
        it is called with 4 params:
            id of the message, time when the message was sent as unix timestamp,
            nickname of the person who sent it, message text itself
*/
function ChatLongPoll(longPollServerURL, lastIdGetURL, newMessageCallback) {
    var mainContext = this,
        httpRequest = createAjax();

    //function that starts long poll and sets what should happen when connection ends
    this.startLongPoll = function() {
        httpRequest.onreadystatechange = function() {
            if (httpRequest.readyState == 4) {
                var _decodedResponse = JSON.parse(httpRequest.responseText);
                if (_decodedResponse.status == 'new') {
                    for (var i = 0; i <_decodedResponse.messages.length; i++) {
                        newMessageCallback(_decodedResponse.messages[i].id, _decodedResponse.messages[i].time, _decodedResponse.messages[i].from, _decodedResponse.messages[i].messageText);
                        mainContext.lastMessageId = _decodedResponse.messages[i].id;
                    }
                }
                mainContext.openLongPoll();
            }
        };
        mainContext.openLongPoll();
    };

    //function that opens connection to the server
    this.openLongPoll = function() {
        httpRequest.open('GET', longPollServerURL + '?lastId=' + mainContext.lastMessageId, true);
        httpRequest.send(null);
    };

    //function that downloads ID of last message sent
    this.getLastMessageId = function() {
        var _id = false;
        httpRequest.onreadystatechange = function() {
            if (httpRequest.readyState == 4) {
                _id = httpRequest.responseText;
            }
        };
        httpRequest.open('GET', lastIdGetURL, false);
        httpRequest.send(null);
        return _id;
    };

    this.lastMessageId = this.getLastMessageId();
}


/*
Class that takes care of sending messages to server
PARAMS:
    newMessageServerURL - string - URL of file, which saves and validates the message
    callback - function - function to handle response from server (ok / wrongFormat / noName / ... status etc... (that depends on how you make your server))
*/
function ChatSendMessage(newMessageServerURL, callback) {
    var httpRequest = createAjax();

    httpRequest.onreadystatechange = function() {
        if (httpRequest.readyState == 4) {
            callback(JSON.parse(httpRequest.responseText));
        }
    };

    //function that sends the message to server and saves it to httpRequest.lastMessage
    this.sendMessage = function (from, messageText) {
        var L_postData = 'from=' + encodeURIComponent(from) + '&messageText=' + encodeURIComponent(messageText);

        httpRequest.lastMessage = messageText;

        httpRequest.open('POST', newMessageServerURL, true);
        httpRequest.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        httpRequest.send(L_postData);
    };
}

/*
Short class that just simplifies access to those 2 classes before
PARAMS:
    newMessageServerURL - string - URL of file, which saves and validates the message
    longPollServerURL - string - URL of server file, which acts as long poll server
    lastIdGetURL - string - URL of txt file, where last message id is saved
    messageReceivedCallback - function - callback to get called when new message is received, it is called with 4 params: id of the message, time when the message was sent as unix timestamp, nickname of the person who sent it, message text itself
    sentMessageCallback - function - function to handle response from server (ok / wrongFormat / noName / ... status etc... (that depends on how you make your server))
*/
function ServerCommunicator(newMessageServerURL, longPollServerURL, lastIdGetURL, messageReceivedCallback, sentMessageCallback) {
    var longPoll = new ChatLongPoll(longPollServerURL, lastIdGetURL, messageReceivedCallback);
    var sendMessage = new ChatSendMessage(newMessageServerURL, sentMessageCallback);

    this.sendMessage = sendMessage.sendMessage;
    this.startLongPoll = longPoll.startLongPoll;
}
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Could you go through your code and explain what you did differently than the OP? Right now this is more of just a code dump, with no explanation of what the OP should do and why. Once you make those changes you can have my upvote. \$\endgroup\$ – syb0rg Apr 26 '14 at 21:37
  • \$\begingroup\$ OK, I will add some comments to the code. \$\endgroup\$ – user2486570 Apr 27 '14 at 14:38
1
\$\begingroup\$

For the XSS part, here's some resources. It would be better off if I just referenced them rather than write the entire thing down. The second one might be the one you want:

By the definition of htmlspecialchars:

Certain characters have special significance in HTML, and should be represented by HTML entities if they are to preserve their meanings. This function returns a string with these conversions made. If you require all input substrings that have associated named entities to be translated, use htmlentities() instead.

You might want to look at htmlentities instead.

Also, in my experience, even if you are safe with just one input, you can still be victim of fragmented XSS, where an XSS attack is broken into parts which actually pass through preventive measures. The last time I did such was on a Wordpress plugin, which also happens to be a chat widget. What I did was carefully craft my XSS where the first half was injected to the username and the second continued on as the message. The first half commented out the code in between until the second half.

For the JS part, mostly reusability and some notes:

// You could factor out the XHR to their own functions and make them handy
// Notes:
// - I haven't seen onload used in xhr. Read more about onreadystatechange event
//   and  readystate properties for cross-browser compatibility. 
var MyAJAX = {
    get : function (url, data,callback) {

      var xhr = new XMLHttpRequest();

      // Serialize
      var serializedData = Object.keys(data).reduce(function(serialized,key,index){
        if(index) serialized += '&';
        serialized += encodeURI(key + '=' data[key]);
        return serialized;
      },'?');

      xhr.open("GET", url + serializedData);    
      xhr.onload = function(e){
        callback.call(null,e.response);
      };
      xhr.send()
    },
    post: function (url, data,callback) {
      var formData = new FormData();
      var xhr = new XMLHttpRequest();

      // For each in data object, append
      Object.keys(data).forEach(function(key){
        fd.append(key,data[key]);
      });

      xhr.open("POST", url);
      xhr.onload = function(e){
        callback.call(null,e.response);
      };
      xhr.send(formData);
    }
}
// Cache DOM elements
// Notes:
// - If you use an ID, you're better off with getElementById rather than querySelectorAll
var usr = "";
var cb = document.getElementById("chatbar");
var chatLog = document.getElementById("chatlog");

// And your AJAX function looks as clean as this when used
setInterval(function(){
  MyAJAX.get('uhh.txt',{
    a : Math.random()
  },function(data){
    chatLog.innerHTML = data.replace(/\n/g, "<br/>");
  });
},1000);


function chat(m) {
  MyAJAX.post('uhh.php',{
    "usr" : usr,
    "msg" : m
  },function(data){
    // Do something when sent
  });
}

// I suggest you don't place this event on window. Place it on the 
// element for efficiency.

cb.addEventListener('keyup',function(e){
  var identifier = e.keyIdentifier;

  if (identifier == "U+00BF") {
    cb.focus();
  } else if (e.keyIdentifier == "Enter") {
    if (usr) {
      chat(cb.value);
      cb.value = "";
    } else {
      usr = cb.value;
      cb.placeholder = "Press / to chat";
      cb.value = "";

      // This only limits this on the browser. There's no one
      // stopping anyone sending more than this without using a browser
      cb.maxLength = 9001;
    }
  }
});
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ Yeah, someone can just use the chat function in the console. \$\endgroup\$ – Cilan Apr 27 '14 at 14:57
  • \$\begingroup\$ @TheWobbuffet That's one issue that can't be solved in JS and should be addressed in the server. Encapsulating the function wouldn't help much. Anyone can replicate the request that chat does, even without a browser with tools like REST testers, curl, wget. \$\endgroup\$ – Joseph Apr 27 '14 at 18:46
  • \$\begingroup\$ Yeah, but I wouldn't call making chosen text appear cracking \$\endgroup\$ – Cilan Aug 2 '14 at 13:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.