近在cnode社区,由@吴中骅的一篇关于XSS的文章,直接导致了社区的人开始在cnode尝试各种攻击,
XSS等web安全漏洞的防范
。这里总结了一下这次碰到的一些问题与解决方案。文件上传漏洞
之前nodeclub在上传图片的时候逻辑是这样的:
<code class="javascript"><span>//用户上传的文件名</span><span>var</span><span>filename</span><span>=</span><span>Date</span><span>.</span><span>now</span><span>()</span><span>+</span><span>'_'</span><span>+</span><span>file</span><span>.</span><span>name</span><span>;</span><span>//用户文件夹</span><span>var</span><span>userDir</span><span>=</span><span>path</span><span>.</span><span>join</span><span>(</span><span>config</span><span>.</span><span>upload_dir</span><span>,</span><span>uid</span><span>);</span><span>//最终文件保存的路径</span><span>var</span><span>savepath</span><span>=</span><span>path</span><span>.</span><span>join</span><span>(</span><span>userDir</span><span>,</span><span>filename</span><span>);</span><span>//将用户上传的文件从临时目录移动到最终保存路径</span><span>fs</span><span>.</span><span>rename</span><span>(</span><span>file</span><span>.</span><span>path</span><span>,</span><span>savepath</span><span>,</span><span>callback</span><span>);</span></code>
看上去好像没有问题,每个人上传的文件都存放在以用户UID命名的一个文件夹内,并且以当前的时间戳作前缀。但是当有用户恶意构造输入的时候,问题就出现了。当用户上传的文件filename为/../../xxx的时候,上传的文件就会rename到用户文件夹之外,导致用户可以替换现有系统上的任何文件。
这个漏洞相对来说非常的低级,但是后果却是最严重的,直接导致整个系统都可能被用户控制。修复的方法也很简单:
<code class="javascript"><span>var</span><span>filename</span><span>=</span><span>Date</span><span>.</span><span>now</span><span>()</span><span>+</span><span>'_'</span><span>+</span><span>file</span><span>.</span><span>name</span><span>;</span><span>var</span><span>userDir</span><span>=</span><span>path</span><span>.</span><span>join</span><span>(</span><span>config</span><span>.</span><span>upload_dir</span><span>,</span><span>uid</span><span>);</span><span>//获取最终保存到的绝对路径</span><span>var</span><span>savepath</span><span>=</span><span>path</span><span>.</span><span>resolve</span><span>(</span><span>path</span><span>.</span><span>join</span><span>(</span><span>userDir</span><span>,</span><span>filename</span><span>));</span><span>//验证</span><span>if</span><span>(</span><span>savepath</span><span>.</span><span>indexOf</span><span>(</span><span>path</span><span>.</span><span>resolve</span><span>(</span><span>userDir</span><span>))</span><span>!==</span><span>0</span><span>)</span><span>{</span><span>return</span><span>res</span><span>.</span><span>send</span><span>({</span><span>status</span><span>:</span><span>'forbidden'</span><span>});</span><span>}</span><span>fs</span><span>.</span><span>rename</span><span>(</span><span>file</span><span>.</span><span>path</span><span>,</span><span>savepath</span><span>,</span><span>callback</span><span>);</span></code>
富文本编辑器的XSS
关于XSS,在@吴中骅的文章中已经非常详细的描述了。而cnode社区中,用户发表话题和回复话题也是用的一个支持markdown格式的富文本编辑器。之前是没有做过任何XSS防范措施的,于是...你可以直接在里面写:
<code class="javascript"><span><</span><span>script</span><span>></span><span>alert</span><span>(</span><span>123</span><span>);</span><span><</span><span>/script></span><span><</span><span>div</span><span>onmouseover</span><span>=</span><span>"alert(123)"</span><span>><</span><span>/div></span><span><</span><span>a</span><span>href</span><span>=</span><span>"javascript.:alert(123);"</span><span>></span><span>123</span><span><</span><span>/a></span></code>
而markdown格式的内容也没有做URL有效性检测,于是各种样式的XSS又出来了:
<code class="javascript"><span>[</span><span>xss</span><span>][</span><span>1</span><span>]</span><span>[</span><span>xss</span><span>][</span><span>2</span><span>]</span><span>!</span><span>[</span><span>xss</span><span>][</span><span>3</span><span>]</span><span>[</span><span>1</span><span>]</span><span>:</span><span>javascript</span><span>:</span><span>alert</span><span>(</span><span>123</span><span>);</span><span>[</span><span>2</span><span>]</span><span>:</span><span>http</span><span>:</span><span>//www.baidu.com/#"onclick='alert(123)'</span><span>[</span><span>3</span><span>]</span><span>:</span><span>http</span><span>:</span><span>//www.baidu.com/img.jpg#"onmouseover='alert(123)'</span></code>
在社区这个应用场景下,引入HTML标签只是为了进行一些排版的操作,而其他的样式定义等等都只会让整个界面一团糟,更别说还有潜在的XSS漏洞风险。因此,其实我们是不需要支持用户输入HTML标签来进行内容排版的,一切都可以通过markdown来代替。然后通过简单粗暴的HTML escape,就可以消灭掉直接输入HTML导致的XSS风险。
<code class="javascript"><span>function</span><span>escape</span><span>(</span><span>html</span><span>)</span><span>{</span><span>return</span><span>html</span><span>.</span><span>replace</span><span>(</span><span>/&(?!\w+;)/g</span><span>,</span><span>'&'</span><span>)</span><span>.</span><span>replace</span><span>(</span><span>/</g</span><span>,</span><span>'<'</span><span>)</span><span>.</span><span>replace</span><span>(</span><span>/>/g</span><span>,</span><span>'>'</span><span>)</span><span>.</span><span>replace</span><span>(</span><span>/"/g</span><span>,</span><span>'"'</span><span>);</span><span>}</span></code>
然而这样粗暴的进行escape,会导致用户输入的代码里面的< > ;这些特殊字符也被转义掉,不能正确显示,需要先将代码段提取出来保存,只转义非代码段的部分,
电脑资料
《XSS等web安全漏洞的防范》(http://meiwen.anslib.com)。于是这个escape函数变成了这样:
<code class="javascript"><span>function</span><span>escape</span><span>(</span><span>html</span><span>)</span><span>{</span><span>var</span><span>codeSpan</span><span>=</span><span>/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm</span><span>;</span><span>var</span><span>codeBlock</span><span>=</span><span>/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g</span><span>;</span><span>var</span><span>spans</span><span>=</span><span>[];</span><span>var</span><span>blocks</span><span>=</span><span>[];</span><span>var</span><span>text</span><span>=</span><span>String</span><span>(</span><span>html</span><span>).</span><span>replace</span><span>(</span><span>/\r\n/g</span><span>,</span><span>'\n'</span><span>)</span><span>.</span><span>replace</span><span>(</span><span>'/\r/g'</span><span>,</span><span>'\n'</span><span>);</span><span>text</span><span>=</span><span>'\n\n'</span><span>+</span><span>text</span><span>+</span><span>'\n\n'</span><span>;</span><span>text</span><span>=</span><span>text</span><span>.</span><span>replace</span><span>(</span><span>codeSpan</span><span>,</span><span>function</span><span>(</span><span>code</span><span>)</span><span>{</span><span>spans</span><span>.</span><span>push</span><span>(</span><span>code</span><span>);</span><span>return</span><span>'`span`'</span><span>;</span><span>});</span><span>text</span><span>+=</span><span>'~0'</span><span>;</span><span>return</span><span>text</span><span>.</span><span>replace</span><span>(</span><span>codeBlock</span><span>,</span><span>function</span><span>(</span><span>whole</span><span>,</span><span>code</span><span>,</span><span>nextChar</span><span>)</span><span>{</span><span>blocks</span><span>.</span><span>push</span><span>(</span><span>code</span><span>);</span><span>return</span><span>'\n\tblock'</span><span>+</span><span>nextChar</span><span>;</span><span>})</span><span>.</span><span>replace</span><span>(</span><span>/&(?!\w+;)/g</span><span>,</span><span>'&'</span><span>)</span><span>.</span><span>replace</span><span>(</span><span>/</g</span><span>,</span><span>'<'</span><span>)</span><span>.</span><span>replace</span><span>(</span><span>/>/g</span><span>,</span><span>'>'</span><span>)</span><span>.</span><span>replace</span><span>(</span><span>/"/g</span><span>,</span><span>'"'</span><span>)</span><span>.</span><span>replace</span><span>(</span><span>/`span`/g</span><span>,</span><span>function</span><span>()</span><span>{</span><span>return</span><span>spans</span><span>.</span><span>shift</span><span>();</span><span>})</span><span>.</span><span>replace</span><span>(</span><span>/\n\tblock/g</span><span>,</span><span>function</span><span>()</span><span>{</span><span>return</span><span>blocks</span><span>.</span><span>shift</span><span>();</span><span>})</span><span>.</span><span>replace</span><span>(</span><span>/~0$/</span><span>,</span><span>''</span><span>)</span><span>.</span><span>replace</span><span>(</span><span>/^\n\n/</span><span>,</span><span>''</span><span>)</span><span>.</span><span>replace</span><span>(</span><span>/\n\n$/</span><span>,</span><span>''</span><span>);</span><span>};</span></code>
而对于markdown生成的标签和标签中的href属性,必须要做URL有效性检测或者做xss的过滤。这样保证通过markdown生成的HTML代码也是没有XSS漏洞的。
因为XSS的手段确实比较多,见XSS Filter Evasion Cheat Sheet。因此能够做粗暴的HTML escape是最安全的,但是并不是每一个地方都可以通过markdown来代替HTML代码,所以不是每一个地方都能用HTML escape,这个时候就需要其他的手段来过滤XSS漏洞了。
XSS防范只能通过定义白名单的形式,例如只允许
现有的XSS过滤模块,一个是node-validator, 一个是@雷宗民写的js-xss。
不能够保证XSS模块可以防范住任意的XSS攻击,但是起码能够过滤掉大部分能够想象到的漏洞。node-validator的XSS()仍然有bug,对于 而所有的过滤必须有一个前提:模版文件中的HTML属性的值等,必须使用双引号。 例如 <code class="javascript"><span><</span><span>img</span><span>src</span><span>=</span><span>'<%= reply.author.avatar_url %>'</span><span>title</span><span>=</span><span>'<%= reply.author.name %>'</span><span>/></span><span><</span><span>img</span><span>src</span><span>=</span><span>"<%= reply.author.avatar_url %>"</span><span>title</span><span>=</span><span>"<%= reply.author.name %>"</span><span>/></span></code>
上面两条语句,第一句由于使用的是单引号,用户可以通过构造一个avatar_url中带单引号,来截断src属性,后面就可以随意加javascript代码了。
CSRF攻击
CSRF攻击在node的web开发框架connect和express等中都有了解决方方案。通过在访客的session中存放一个随机的_csrf字段,模版引擎在生成HTML文件的时候将这个_csrf值传递到前端,访客提交的任意POST请求,都必须带上这个字段进行验证,保证了只有当前用户在当前页面上可以进行修改的操作。
然而当页面存在XSS漏洞的时候,CSRF的这种防范措施就成了浮云。恶意攻击者完全可以通过javascript代码,获取到其他用户的_csrf值,并直接模拟用户的POST请求进行服务端数据的更改