PHP防止XSS攻击之过滤、验证和转义之转义的例子

PHP 转义实现

把输出渲染成网页或API响应时,一定要转义输出,这也是一种防护措施,能避免渲染恶意代码,造成XSS攻击,还能防止应用的用户无意中执行恶意代码。

我们可以使用前面提到的htmlentities函数转移输出,该函数的第二个参数一定要使用ENT_QUOTES,让这个函数转义单引号和双引号,而且,还要在第三个参数中指定合适的字符编码(通常是UTF-8),下面的例子演示了如何在渲染前转义HTML输出:

<?php
$output = '<p><script>alert(“欢迎来到Laravel学院!")</script></p>';
echo htmlentities($output, ENT_QUOTES, ‘UTF-8');
如果不转义直接输出,会弹出提示框:

alert

转义之后输出变成:

<p><script>alert("欢迎访问Laravel学院!");</script></p>
现代PHP支持许多模板引擎,这些模板引擎在底层已经为了做好了转义处理,比如现在流行的twig/twig和smarty/smarty都会自动转义输出。这种默认处理方式很赞,为PHP Web应用提供了有力的安全保障。

Blade 模板引擎避免XSS攻击原理
Laravel使用的模板引擎是Blade,关于Blade的使用可以参考其官方文档,这里我们简单探讨下Laravel底层如何对输出进行转义处理。

一般我们在Laravel中返回视图内容会这么做:

return view(’test’, [‘data’=>$data]);
这是一个很简单的例子,意味着我们会在resources/views目录下找到test.blade.php视图文件,然后将$data变量传入其中,并将最终渲染结果作为响应的内容返回给用户。那么这一过程经历了哪些底层源码的处理,如果$data变量中包含脚本代码(如JavaScript脚本),又该怎么去处理呢?接下来我们让来一窥究竟。

首先我们从辅助函数view入手,当然这里我们也可以使用View:make,但是简单起见,我们一般用view函数,该函数定义在Illuminate\Foundation\helpers.php文件中:

function view($view = null, $data = [], $mergeData = [])
{
    $factory = app(ViewFactory::class);
    if (func_num_args() === 0) {
        return $factory;
    }

    return $factory->make($view, $data, $mergeData);
}
该函数中的逻辑是从容器中取出视图工厂接口ViewFactory对应的实例$factory(该绑定关系在Illuminate\View\ViewServiceProvider的register方法中注册,此外这里还注册了模板引擎解析器EngineResolver,包括PhpEngine和载入BladeCompiler的CompilerEngine,以及视图文件查找器FileViewFinder,一句话,这里注册了视图解析所需的所有服务),如果传入了参数,则调用$factory上的make方法:

public function make($view, $data = [], $mergeData = [])
{
    if (isset($this->aliases[$view])) {
        $view = $this->aliases[$view];
    }

    $view = $this->normalizeName($view);

    $path = $this->finder->find($view);

    $data = array_merge($mergeData, $this->parseData($data));

    $this->callCreator($view = new View($this, $this->getEngineFromPath($path), $view, $path, $data));

    return $view;
}
这个方法位于Illuminate\View\Factory,这里所做的事情是获取视图文件的完整路径,合并传入变量,$this->getEngineFromPath会通过视图文件后缀获取相应的模板引擎,比如我们使用.blade.php结尾的视图文件则获得到的是CompilerEngine(即Blade模板引擎),否则将获取到PhpEngine,然后我们根据相应参数实例化View(Illuminate\View\View)对象并返回。需要注意的是View类中重写了__toString方法:

public function __toString()
{
    return $this->render();
}
所以当我们打印$view实例的时候,实际上会调用View类的render方法,所以下一步我们理所应当研究render方法做了些什么:

public function render(callable $callback = null)
{
    try {
        $contents = $this->renderContents();
        $response = isset($callback) ? call_user_func($callback, $this, $contents) : null;

        // Once we have the contents of the view, we will flush the sections if we are
        // done rendering all views so that there is nothing left hanging over when
        // another view gets rendered in the future by the application developer.
        $this->factory->flushSectionsIfDoneRendering();

        return ! is_null($response) ? $response : $contents;
    } catch (Exception $e) {
        $this->factory->flushSections();

        throw $e;
    } catch (Throwable $e) {
        $this->factory->flushSections();
 
        throw $e;
    }
}
这里重点是$this->renderContents()方法,我们继续深入研究View类中的renderContents方法:

protected function renderContents()
{
    // We will keep track of the amount of views being rendered so we can flush
    // the section after the complete rendering operation is done. This will
    // clear out the sections for any separate views that may be rendered.
    $this->factory->incrementRender();
    $this->factory->callComposer($this);

    $contents = $this->getContents();

    // Once we've finished rendering the view, we'll decrement the render count
    // so that each sections get flushed out next time a view is created and
    // no old sections are staying around in the memory of an environment.
    $this->factory->decrementRender();

    return $contents;
}
我们重点关注$this->getContents()这里,进入getContents方法:

protected function getContents()
{
    return $this->engine->get($this->path, $this->gatherData());
}
我们在前面已经提到,这里的$this->engine对应CompilerEngine(Illuminate\View\Engines\CompilerEngine),所以我们进入CompilerEngine的get方法:

public function get($path, array $data = [])
{
    $this->lastCompiled[] = $path;
    // If this given view has expired, which means it has simply been edited since
    // it was last compiled, we will re-compile the views so we can evaluate a
    // fresh copy of the view. We'll pass the compiler the path of the view.
    if ($this->compiler->isExpired($path)) {
        $this->compiler->compile($path);
    }

    $compiled = $this->compiler->getCompiledPath($path);

    // Once we have the path to the compiled file, we will evaluate the paths with
    // typical PHP just like any other templates. We also keep a stack of views
    // which have been rendered for right exception messages to be generated.
    $results = $this->evaluatePath($compiled, $data);

    array_pop($this->lastCompiled);

    return $results;
}
同样我们在之前提到,CompilerEngine使用的compiler是BladeCompiler,所以$this->compiler也就是Blade编译器,我们先看$this->compiler->compile($path);这一行(首次运行或者编译好的视图模板已过期会进这里),进入BladeCompiler的compile方法:

public function compile($path = null)
{
    if ($path) {
        $this->setPath($path);
    }
    if (! is_null($this->cachePath)) {
        $contents = $this->compileString($this->files->get($this->getPath()));

        $this->files->put($this->getCompiledPath($this->getPath()), $contents);
    }
}
这里我们做的事情是先编译视图文件内容,然后将编译好的内容存放到视图编译路径(storage\framework\views)下对应的文件(一次编译,多次运行,以提高性能),这里我们重点关注的是$this->compileString方法,该方法中使用了token_get_all函数将视图文件代码分割成多个片段,如果片段是数组的话则循环调用$this->parseToken方法:

protected function parseToken($token)
{
    list($id, $content) = $token;
    if ($id == T_INLINE_HTML) {
        foreach ($this->compilers as $type) {
            $content = $this->{"compile{$type}"}($content);
        }
    }

    return $content;
}
来到这里,我们已经很接近真相了,针对HTML代码(含Blade指令代码),循环调用compileExtensions、compileStatements、compileComments和compileEchos方法,我们重点关注输出方法compileEchos,Blade引擎默认提供了compileRawEchos、compileEscapedEchos和compileRegularEchos三种输出方法,对应的指令分别是{!! !!}、{{{ }}}和{{ }},顾名思义,compileRawEchos对应的是原生输出:

protected function compileRawEchos($value)
{
    $pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->rawTags[0], $this->rawTags[1]);
    $callback = function ($matches) {
        $whitespace = empty($matches[3]) ? '' : $matches[3].$matches[3];

        return $matches[1] ? substr($matches[0], 1) : '<?php echo '.$this->compileEchoDefaults($matches[2]).'; ?>'.$whitespace;
    };

    return preg_replace_callback($pattern, $callback, $value);
}
即Blade视图中以{!! !!}包裹的变量会原生输出HTML,如果要显示图片、链接,推荐这种方式。

{{{}}}对应的CompileEscapedEchos,这个在Laravel 4.2及以前版本中用于转义,现在已经替换成了{{}},即调用compileRegularEchos方法:

protected function compileRegularEchos($value)
{
    $pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->contentTags[0], $this->contentTags[1]);
    $callback = function ($matches) {
        $whitespace = empty($matches[3]) ? '' : $matches[3].$matches[3];

        $wrapped = sprintf($this->echoFormat, $this->compileEchoDefaults($matches[2]));

        return $matches[1] ? substr($matches[0], 1) : '<?php echo '.$wrapped.'; ?>'.$whitespace;
    };

    return preg_replace_callback($pattern, $callback, $value);
}
其中$this->echoFormat对应e(%s),无独有偶,compileEscapedEchos中也用到这个方法:

protected function compileEscapedEchos($value)
{
    $pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->escapedTags[0], $this->escapedTags[1]);
    $callback = function ($matches) {
        $whitespace = empty($matches[3]) ? '' : $matches[3].$matches[3];

        return $matches[1] ? $matches[0] : '<?php echo e('.$this->compileEchoDefaults($matches[2]).'); ?>'.$whitespace;
    };

    return preg_replace_callback($pattern, $callback, $value);

}
辅助函数e()定义在Illuminate\Support\helpers.php中:

function e($value)
{
    if ($value instanceof Htmlable) {
        return $value->toHtml();
    }
    return htmlentities($value, ENT_QUOTES, 'UTF-8', false);
}
其作用就是对输入的值进行转义。

经过这样的转义,视图中的{{ $data }}或被编译成<?php echo $data?>,最终如何将$data传入视图输出,我们再回到CompilerEngine的get方法,看这一段:

$results = $this->evaluatePath($compiled, $data);
evaluatePath中传入了编译后的视图文件路径和传入的变量$data,该方法定义如下:

protected function evaluatePath($__path, $__data)
{
   $obLevel = ob_get_level();ob_start();

    extract($__data, EXTR_SKIP);

    // We'll evaluate the contents of the view inside a try/catch block so we can
    // flush out any stray output that might get out before an error occurs or
    // an exception is thrown. This prevents any partial views from leaking.
    try {
        include $__path;
    } catch (Exception $e) {
        $this->handleViewException($e, $obLevel);
    } catch (Throwable $e) {
        $this->handleViewException(new FatalThrowableError($e), $obLevel);
    }

    return ltrim(ob_get_clean());
}
这里面调用了PHP系统函数extract将传入变量从数组中导入当前符号表(通过include $__path引入),其作用也就是将编译后视图文件中的变量悉数替换成传入的变量值(通过键名映射)。

好了,这就是Blade视图模板从渲染到输出的基本过程,可以看到我们通过{{}}来转义输出,从而达到避免XSS攻击的目的。

时间: 2024-09-28 22:45:48

PHP防止XSS攻击之过滤、验证和转义之转义的例子的相关文章

整理php防注入和XSS攻击通用过滤_php技巧

对网站发动XSS攻击的方式有很多种,仅仅使用php的一些内置过滤函数是对付不了的,即使你将filter_var,mysql_real_escape_string,htmlentities,htmlspecialchars,strip_tags这些函数都使用上了也不一定能保证绝对的安全. 那么如何预防 XSS 注入?主要还是需要在用户数据过滤方面得考虑周全,在这里不完全总结下几个 Tips 1. 假定所有的用户输入数据都是"邪恶"的 2. 弱类型的脚本语言必须保证类型和期望的一致 3.

php预防XSS攻击,ajax跨域攻击的方法

对网站发动XSS攻击的方式有很多种,仅仅使用php的一些内置过滤函数是对付不了的,即使你将filter_var,mysql_real_escape_string,htmlentities,htmlspecialchars,strip_tags这些函数都使用上了也不一定能保证绝对的安全. 现在有很多php开发框架都提供关于防XSS攻击的过滤方法,下面和大家分享一个预防XSS攻击和ajax跨域攻击的函数,摘自某开发框架,相比于仅仅使用内置函数应该还是够强了的吧. function xss_clean

预防XSS攻击,(参数/响应值)特殊字符过滤

一.什么是XSS攻击 XSS是一种经常出现在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中.比如这些代码包括HTML代码和客户端脚本.攻击者利用XSS漏洞旁路掉访问控制--例如同源策略(same origin policy).这种类型的漏洞由于被黑客用来编写危害性更大的网络钓鱼(Phishing)攻击而变得广为人知.对于跨站脚本攻击,黑客界共识是:跨站脚本攻击是新型的"缓冲区溢出攻击",而JavaScript是新型的"ShellCod

前端安全之XSS攻击

 XSS(cross-site scripting跨域脚本攻击)攻击是最常见的Web攻击,其重点是"跨域"和"客户端执行".有人将XSS攻击分为三种,分别是: 1. Reflected XSS(基于反射的XSS攻击) 2. Stored XSS(基于存储的XSS攻击) 3. DOM-based or local XSS(基于DOM或本地的XSS攻击) Reflected XSS 基于反射的XSS攻击,主要依靠站点服务端返回脚本,在客户端触发执行从而发起Web攻击.

Jsoup代码解读之八-防御XSS攻击

防御XSS攻击的一般原理 cleaner是Jsoup的重要功能之一,我们常用它来进行富文本输入中的XSS防御. 我们知道,XSS攻击的一般方式是,通过在页面输入中嵌入一段恶意脚本,对输出时的DOM结构进行修改,从而达到执行这段脚本的目的.对于纯文本输入,过滤/转义HTML特殊字符<,>,",'是行之有效的办法,但是如果本身http://www.aliyun.com/zixun/aggregation/18678.html">用户输入的就是一段HTML文本(例如博客文章

防御XSS攻击的七条原则

前言 本文将会着重介绍防御XSS攻击的一些原则,需要读者对于XSS有所了解,至少知道XSS漏洞的基本原理,如果您对此不是特别清楚,请参考这两篇文章:<Stored and Reflected XSS Attack><DOM Based XSS> 攻击者可以利用XSS漏洞向用户发送攻击脚本,而用户的浏览器因为没有办法知道这段脚本是不可信的,所以依然会执行它.对于浏览器而言,它认为这段脚本是来自可以信任的服务器的,所以脚本可以光明正大地访问Cookie,或者保存在浏览器里被当前网站所用

WEB测试番外之----XSS攻击

1.1 什么是XSS攻击 XSS攻击:跨站脚本攻击(Cross Site Scripting),为不和层叠样式表(Cascading Style. Sheets, CSS)的缩写混淆.故将跨站脚本攻击缩写为XSS.XSS是一种经常出现在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中.比如这些代码包括HTML代码和客户端脚本.攻击者利用XSS漏洞旁路掉访问控制--例如同源策略(same origin policy).这种类型的漏洞由于被骇客用来编写危害性更

浅谈跨网站脚本攻击(XSS)的手段与防范(简析新浪微博XSS攻击事件)

本文主要涉及内容: 什么是XSS XSS攻击手段和目的 XSS的防范 新浪微博攻击事件 什么是XSS 跨网站脚本(Cross-sitescripting,通常简称为XSS或跨站脚本或跨站脚本攻击)是一种网站应用程序的安全漏洞攻击,是代码注入的一种.它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响.这类攻击通常包含了HTML以及用户端脚本语言. XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序.这些恶

XSS攻击的解决方法

在我上一篇<前端安全之XSS攻击>文中,并没有把XSS攻击的解决办法说完整,而XSS的攻击又那么五花八门,有没有一招"独孤九剑"能够抗衡,毕竟那么多情况场景,开发人员无法一一照顾过来,而今天通过阅读<白帽子讲Web安全>这本书,对应对方式有了更好的总结,分为两类,一是服务端可以干的事,二是客户端可以干的事. 前提 在说XSS解决方式时,有一个前提.就是同源策略--浏览器的同源策略(浏览器安全的基础,即使是攻击脚本也要遵守这法则),限制了来自不同源的"d