GitHub地址: https://github.com/dotboris/vuejs-serverside-template-xss
这个repository演示了使用服务端和Vue.js混合渲染的demo,即使采取了预防措施,也是容易受到XSS攻击的。
index.php
是一个有漏洞的PHP脚本. fix-v-pre.php
和fix-servervars-global.php
是漏洞脚本的修复版. README 围绕 怎么利用这个漏洞, 展示怎么修补这个漏洞并讨论这样的漏洞影响范围.
这个漏洞并没有使用特定的Vue.js和PHP, 如果你有一个程序混合使用了服务端渲染和客户端渲染, 就有可能存在漏洞。
我建议,运行这个demo,尝试利用,并尝试修复,这是一个很好的学习机会。
运行demo
-
运行程序
docker-compose up -d
-
在浏览器打开 http://localhost:8080
如果你不想使用docker, 直接将文件放在php服务上,并使得可以访问。
请记住,这个文件被设计用于XSS的漏洞,你应该在本地环境运行。
演练
首先,我们打开应用程序,看看我们有两件事可以做:
- 允许输入的文本框
- 一个计数器
当我们输入字符到文本框时,看到我们放进去的任何东西都会变成查询参数。如果我们把“FoBar”放在文本框中,我们将得到以下URL:index.php?injectme=FoBar
我们可以看到文本“FoBar”被注入页面中。
这个注入看起来是在服务器端完成的。我们可以通过查看页面的源代码来确认这一点。我们看到“FoBaar”是服务器发送的响应的一部分。我们这里有XSS的机会。让我们试试常用的技巧。
当我们使用 <script>alert('xss')</script>
我们获得了 <script>alert('xss')</script>
.
类似的尝试 <img src="nope.jpg" onerror="alert('xss')"/>
获得 <img src="nope.jpg" onerror="alert('xss')"/>
.
看起来所有都转码了。我们可以通过查看源代码来确定。让我们来理解程序是怎么工作的。这可能对我们有帮助
这个计数器使用Vue.js构建。对不熟悉的人来说Vue.js是一个运行在浏览器的javascript库,他可以用来编译动态的前端程序。
一种使用Vue.js的方法是在页面html中编写模板。然后Vue.js通过JavaScript来渲染。当你有一个程序使用服务端渲染,你想给他添加一些动态性,这个是一个很常见的事情,这也是现在正在发生的。
当我们看这个模板时我们看到我们输入的值直接渲染在了模板内部
<div id="injectable-app">
<div>
You have injected: OUR_INJECTED_VALUE_GOES_HERE
</div>
<button type="button" @click="dec">-</button>
{{counter}}
<button type="button" @click="inc">+</button>
</div>
也许我们可以利用这一点。vue.js模板运行你使用表达式,表达式是一些数据的代码,用来转换数据并输出,他们基本上是JavaScript代码。在Vue.js中表达式基本上是{{ ... code goes here ... }}
的形式。
让我们来试试,我们在输入框中输入{{ 2 + 2 }}
,我们得到了You have injected: 4
。
这个一个很好的方法,现在我们来弹出一个alert
来证明我们有完全的控制权。
我们输入一个{{ alert('xss') }}
到输入框,但是什么都没有发生,并且计数器不见了。当查看控制台时,可以看到如下错误:
vue.js:1719 TypeError: alert is not a function
at Proxy.eval (eval at createFunction (vue.js:10518), <anonymous>:3:114)
at Vue$3.Vue._render (vue.js:4465)
at Vue$3.updateComponent (vue.js:2765)
at Watcher.get (vue.js:3113)
at new Watcher (vue.js:3102)
at mountComponent (vue.js:2772)
at Vue$3.$mount (vue.js:8416)
at Vue$3.$mount (vue.js:10777)
at Vue$3.Vue._init (vue.js:4557)
at new Vue$3 (vue.js:4646)
alert
不是一个函数? 这里发生了什么? Vue.js表达式在Vue
实例中查找内容, 并渲染。换句话说, 当我们尝试渲染{{foobar}}
时, 会在模板数据中查找foobar
属性,他被解释为templateData.alert('xss')
,我们得到了一个错误,因为我们并没有一个叫做alert
的属性
可以认为就像被困在了JavaScript的监狱或沙盒,重要的提示是,Vue.js并没有一个正真的沙盒。他不会主动的阻止你访问模板数据以外的东西。这只是渲染表达式导致的。
我们怎么到"沙盒"之外呢?有许多方法,如果你想显示的的JavaScript能力,你可以试试。我给的解决方案是:
{{ constructor.constructor("alert('xss')")() }}
看起来不明显,但是非常简单。我们知道是根据模板来渲染的。当我们输入constructor
被渲染为templateData.constructor
.我们的模板数据是一个对象。在JavaScript中有对象都有一个 constructor。 所以constructor
给我们 Vue$3
(Vue.js的构造函数).
在JavaScript中所有的构造函数都是函数,所有函数都是对象。 这意味着Vue$3
有一个构造器。这个构造器是一个Function
构造器,constructor.constructor
给我们 Function
构造器。
Function
构造器让我们在运行时动态定义函数, 我们传递我们函数代码他将返回一个函数使得我们可以运行。在这个情况下,我们使用Function("alert('xss')")()
结束。就创建了一个叫alert
的函数(真正的alert在全局作用域中),并调用他。
就这样我们做到了。在我们无法控制的页面中注入了一个JavaScript脚本,并且这个JavaScript访问了全局作用域。有了这一点我们可以做任何浏览器可以做的事情。
为什么做这个工作?
这个demo能够被注入是因为程序混合使用了服务端渲染和客户端渲染。
在这个例子中, 我们通过php程序来接受用户输入的参数,并渲染为html页面,而这个程序的转码输入成为html实体来确保不存在简单的XSS。当页面进入浏览器时,vue.js负责html部分,并像模板一样渲染,我们看到了,他在页面上基本上等于做了一个复杂的eval
在这个情况下,Vue.js 不能区分可能不安全的用户输入和被认为安全的基础模板之间的差异。
当可能区分用户输入和模板代码之间的差异的时候, Vue.js在阻止xss上做了非常出色的工作。事实上, 他的工作做得比PHP好,因为他默认认为他将处理的模板数据是不安全的,并总是转码。你不需要记住去转码你的数据。
我要怎么保护?
修复这个问题最简单的方法是当你注入服务端变量到客户端模板时使用 v-pre
directive
在这个例子中,你可以修改
<div id="injectable-app">
<div>
You have injected:
<?= htmlspecialchars($_GET['injectme']) ?>
</div>
<button type="button" @click="dec">-</button>
{{counter}}
<button type="button" @click="inc">+</button>
</div>
为
<div id="injectable-app">
<div v-pre>
You have injected:
<?= htmlspecialchars($_GET['injectme']) ?>
</div>
<button type="button" @click="dec">-</button>
{{counter}}
<button type="button" @click="inc">+</button>
</div>
虽然这个解决方案可行,但效果并不理想。任意一个人都容易忘记使用v-pre
指令。如果一个开发人员忘记做这件事,又会出现一个漏洞。
当谈到安全问题,我更喜欢系统的解决方案。更好的解决方案是在页面中定义一个全局变量,其中包含所有服务器端变量。这并不能阻止开发人员混淆服务端和客户端,但是他提供一个安全的机制来从服务端传递变量到客户端。
我们可以这样实现:
<div id="injectable-app">
<div>
You have injected: {{ SERVER_VARS.injectMe }}
</div>
<button type="button" @click="dec">-</button>
{{counter}}
<button type="button" @click="inc">+</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.13/dist/vue.js"></script>
<?php
$serverVars = [
'injectMe' => (string) $_GET['injectme']
];
?>
<script>
window.SERVER_VARS = JSON.parse(atob('<?= base64_encode(json_encode($serverVars)) ?>'));
Vue.prototype.SERVER_VARS = window.SERVER_VARS;
</script>
完整的修复在fix-servervars-global.php
中.
这是真正的威胁吗?
读到这,你可能会想:为什么会有人会混合使用服务端渲染和客户端渲染。
我认为,一个开发者添加Vue.js到他们已经存在的服务端渲染程序中是非常合理的。并且一切都会很好。Vue.js将自己称为渐进框架。他们希望你这么做。另外,安全风险并没有立即出现,是的我们的XSS运行并不那么直接。
如果你查了一点Google,你会发现一些关于如何使用Vue.js和其他服务器端渲染框架的例子和教程。虽然我没数据支持这个观点,但是我认为还是有很多程序使用了服务器渲染和客户端渲染的混合方式。所有这些程序都有可能受到这种XSS的影响。
这个只针对于PHP吗?
不是的,他可以在任意服务端语言或技术中运行。您可以在任何服务器端技术中使用该示例并重写它,但它仍然是个漏洞。不管这个技术有多大的自动转码都没有关系,因为没有一个自动转码的变量输出到VUE.JS模板中。
那其他的框架或库呢?
任何允许在HTML中编写模板的库或框架都可能受到此攻击。
Angular 1 有个著名的漏洞. 在他们的 安全向导中 angular 团队明确地警告了这一点.
Generating AngularJS templates on the server containing user-provided content. This is the most common pitfall where you are generating HTML via some server-side engine such as PHP, Java or ASP.NET.
他们也尝试了很长时间来构建一个沙盒,以减轻来自服务器的XSS。每当他们改进或修复沙盒时,它就被打破或绕过。最终 angular 团队放弃了沙盒.
像React和Angular 2+这样的框架不太可能会受到这种攻击,因为它们不允许你用HTML编写模板,而且还强迫你使用编译器。这使得从服务器向客户端模板注入用户输入的可能性非常小。我并不是说React和Angualar 2+是防XSS的。你只需要更努力地去做到更难被攻击。
网友评论