-
-
Save kirkbushell/5d40fdd7f7b364716742 to your computer and use it in GitHub Desktop.
class XSSProtection | |
{ | |
/** | |
* The following method loops through all request input and strips out all tags from | |
* the request. This to ensure that users are unable to set ANY HTML within the form | |
* submissions, but also cleans up input. | |
* | |
* @param Request $request | |
* @param callable $next | |
* @return mixed | |
*/ | |
public function handle(Request $request, \Closure $next) | |
{ | |
if (!in_array(strtolower($request->method()), ['put', 'post'])) { | |
return $next($request); | |
} | |
$input = $request->all(); | |
array_walk_recursive($input, function(&$input) { | |
$input = strip_tags($input); | |
}); | |
$request->merge($input); | |
return $next($request); | |
} | |
} |
I had to do array_walk_recursive to get this to work
@opb ah, good idea - that'll solve nested array issues - thanks :)
@misterdesign - thanks, updated :)
Worked out recently that this is screwing with JSON POSTs, as everything gets cast to a string when strip_tags()
is used. So I amended in my code:
if(is_string($input)) $input = strip_tags($input);
Hello,
Nice job, but don't forget when you've multidimensional array in your request :) , so this is my code
function walk($input){
array_walk($input, function(&$input) {
if(!is_array($input)){
$input = strip_tags($input);
}else{
walk($input);
}
});
return $input;
}
$input = walk($input);
$request->merge($input);
return $next($request);
Thank you :D
Would it be better to use htmlspecialchars
instead of strip_tags
?
Using strip_tags
, if an input contains <>
it would be stripped as well. While in reality it isn't harmful and there are some cases where you might want to allow those kind of input (writing a web page about SQL query example come to mind).
Using htmlspecialchars
, the output is displayed as is, with all the tags intact but they're will not mess with layout or be executed by the browser.
@kirkbushell Thanks for the script.
Taking on board other comments I've extended it to match what we needed and thought it might help others. If you have an input field called enquiry_field and you are wanting to accept html in that field, then if the form also passed through a hidden enquiry_field_html with any value in, this will tell the middleware to not feed it through the strip_tags but to use DomDocument and remove script tags and iframe tags instead, leaving the other, safer formatting tags. Ive also added an extra check that says that if its an admin user (via Sentinel) then dont apply the middleware as it will be handling content management requests with script tags anyway
public function handle($request, Closure $next)
{
if (!in_array(strtolower($request->method()), ['put', 'post'])) {
return $next($request);
}
/*
* Admin users are creating web pages in the cms and they are trusted to know what they are doing
* and need to be able to save html with script tags in so if we are an admin, then skip the check
*/
$skipCheck = false;
if ($user = Sentinel::getUser())
{
if ($user->inRole('admin'))
{
$skipCheck = true;
}
}
$input = $request->all();
if (!$skipCheck) {
array_walk_recursive($input, function (&$input, $key) use ($request) {
/*
We are now only doing this if we are not logged in or we are logged in as a normal user.
If we have an input called enquiry_field and we also have one called enquiry_field_html
then dont do a strip_tags to remove all tags because we want to allow tags
but instead load the html into DomDocument, wrapping it with a structurally correct html
tag as we only have a html snippet being saved and extract the script and iframe tags
*/
if (strlen($input)>0) {
if (!$request->get($key . "_html")) {
if (is_string($input)) $input = strip_tags($input);
} else {
$dom = new \DOMDocument();
$html = <<<DOM
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
</head>
<body>${input}</body></html>
DOM;
$dom->loadHTML($html);
$script = $dom->getElementsByTagName('script');
foreach ($script as $item) {
$item->parentNode->removeChild($item);
}
$iframe = $dom->getElementsByTagName('iframe');
foreach ($iframe as $item) {
$item->parentNode->removeChild($item);
}
$body = $dom->getElementsByTagName("body");
if ($body && $body->length>0){
$bodyNode = $body->item(0);
$inputWithBody = $dom->saveHTML($bodyNode);
$matches=[];
preg_match("/<body[^>]*>(.*?)<\/body>/is",$inputWithBody,$matches);
if (count($matches)>1) $input=$matches[1];
}
}
}
});
}
$request->merge($input);
return $next($request);
}
We're using htmlspecialchars
in our project because of what @jackyef mentioned.
// Encode <, > and & as their HTML5 entity equivalent, leaving quotes, accented
// and special characters untouched.
$input = htmlspecialchars($input, ENT_NOQUOTES | ENT_HTML5);
I'm also thinking of replacing every EOL with a single char \n
style to ensure character counts aren't failing because of this.
$input = preg_replace('~\r\n?~', "\n", $input);
Hi. I introduced an arrangement that removes all special characters.
<?php
namespace App\Http\Middleware;
use Illuminate\Support\Facades\Redirect;
use Closure;
class XSS
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$url = str_replace($request->url(), "", $request->fullUrl());
$input = $request->all();
array_walk_recursive($input, function (&$input) {
$input = strip_tags($input);
});
if (preg_match('/[\'^£$%&*()}{@#~><>|_+¬-]/', $url))
return redirect($request->url() . "/" . preg_replace('/[\'^£$%&*()}{@#~><>|_+¬-]/',"",strip_tags($url)));
$request->merge($input);
return $next($request);
}
}
$request->method() must be forced to lower... in_array is case sensitive.