php 转义 实现 把输出渲染成网页或api响应时,一定要转义输出,这也是一种防护措施,能避免渲染恶意代码,造成xss攻击,还能防止应用的用户无意中执行恶意代码。
我们可以使用前面提到的 htmlentities 函数转移输出,该函数的第二个参数一定要使用 ent_quotes ,让这个函数转义单引号和双引号,而且,还要在第三个参数中指定合适的字符编码(通常是utf-8),下面的例子演示了如何在渲染前转义html输出:
$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) : '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) : ''.$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] : '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 }} 或被编译成 ,最终如何将 $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攻击的目的。