I am currently working on a project that requires users to input text that may or may not be formatted (if it helps, just think of it as a Stack Exchange clone, even though that's not quite accurate). Instead of opting for some sort of BB Code or other markup language to allow users to format their text, I figure it'd probably less stressful for the user to just use a subset of HTML. My users are all generally pretty experienced computer users, so I don't expect that to be much of an issue.
My plan is to have a whitelist of tags and attributes. Anything that doesn't match the whitelist will be removed. I figure that as long as I don't accidentally allow a dangerous attribute or element and I implement the code well, there won't be any problems.
I think I generally implemented the code well, but my knowledge of security issues is shaky at best, hence why I'm posting the code here.
public function set($value){
/*
First, I want to make sure that we're dealing with a string, so I cast it to a string. Should I throw an error if it's not a string instead?
Next, I transform \n to <br>, so that whitespace appears correctly. I think it might be better to skip this step and use a CSS rule to allow whitespace.
*/
$value = (string) $value;
$value = nl2br(trim($value));
/*
Here is my whitelist. Notice the multiple br elements.
*/
$value = strip_tags($value, "<a><img><table><tr><td><th><h2><h3><h4><blockquote><ul><ol><li><br><br/><br />");
/*
This caused some issues in my tests, where it'd unnecessarily escape certain things, so I commented it out. Are shell command injections something I need to worry about?
*/
//$value = escapeshellcmd($value);
$dom = new DOMDocument;
if($value === ""){
return false;
}
$dom->loadHTML($value);
$nodes = $dom->getElementsByTagName('*');
foreach($nodes as $node){
if($node->hasAttributes()){
foreach($node->attributes as $attr){
$name = $attr->name;
$value = $attr->value;
switch($name){
/*
These elements are generally safe. Worst case scenario, someone just creates some weird looking HTML, which I'm not too worried about.
*/
case "id": break;
case "style": break;
case "src": break;
case "alt": break;
case "class": break;
/*
This is my biggest concern. You can embed inline javascript with <a href="javascript:foo();"></a>, so I want to prevent this. However, links are important so I don't want to completely strip this attribute.
*/
case "href":
if(strpos($value, "javascript") !== false){
$node->removeAttribute($name);
}
break;
default:
$node->removeAttribute($name);
break;
}
}
}
}
$value = $dom->saveXML($dom->documentElement->firstChild);
/*
PHP is weird and adds a bunch of extra elements around the HTML, and it's easiest to just strip the tags again. It's a little hacky, but it works every time (I think).
*/
$value = strip_tags($value, "<a><img><table><tr><td><th><h2><h3><h4><blockquote><ul><ol><li><br><br/><br />");
/*
I don't exactly remember why this is here. I think it ensures that all br elements are in the same format, but it probably doesn't do a very good job at it.
*/
$value = str_replace("br/", "br", $value);
/*
This ensures that our input is the right length. I chose to do this after we removed all the junk just as a little present for the user.
*/
if(strlen($value) > $this->length){
$this->text = substr($value, 0, $this->length);
}else{
$this->text = $value;
}
return $this;
}
I'm not too worried about SQL injection so my biggest concern is XSS and I guess any forms of injection that I'm not familiar with.
Here's a link to a more testable version of the code.
Changes that I'll be making:
- Use a case insensitive search when checking URLs for "javascript:"
- Remove the style attribute from the whitelist (now I need a way for users to bold/italicize their text, I'll probably allow
<b>
and<i>
, I guess) - Check image src's against a whitelist
- Remove the
nl2br
stuff and usewhite-space: pre-wrap
in my CSS instead
strip_tags
may be a little too much. You get questions on Stack Overflow like "I tried (newline) it didn't work", where it makes no sense because the HTML example is hidden. \$\endgroup\$strip_tags
is appropriate. My users won't be posting code snippets, so I don't need to worry about that. \$\endgroup\$