Skip to content

Instantly share code, notes, and snippets.

@kirkbushell
Last active November 5, 2021 06:08
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save kirkbushell/5d40fdd7f7b364716742 to your computer and use it in GitHub Desktop.
Save kirkbushell/5d40fdd7f7b364716742 to your computer and use it in GitHub Desktop.
Laravel 5 XSS protection middleware
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);
}
}
@misterdesign
Copy link

$request->method() must be forced to lower... in_array is case sensitive.

@opb
Copy link

opb commented Apr 1, 2015

I had to do array_walk_recursive to get this to work

@kirkbushell
Copy link
Author

@opb ah, good idea - that'll solve nested array issues - thanks :)

@misterdesign - thanks, updated :)

@opb
Copy link

opb commented Jul 23, 2015

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);

@safallah
Copy link

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

@jackyef
Copy link

jackyef commented Apr 25, 2017

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.

image

@tonybyng
Copy link

@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);
    }

@emileber
Copy link

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);

@cinkagan
Copy link

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);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment