大纲

  • XSS
  • CSRF
  • Cookie安全性
  • 点击劫持
  • 传输安全问题
  • 用户密码安全
  • SQL注入
  • 信息泄露&社会工程学

XSS利用的是用户对网站的信任,CSRF利用的是网站对用户的信任。

XSS

跨站脚本攻击(Cross Site Scripting)其简称应该是CSS,但是CSS与前端领域的样式表重叠了,所以改为XSS,这个X其实是Cross的通用英文缩写
。XSSg攻击的原理是数据变成了脚本,常用的探测方法就是<script>alert(1)</script>这一条如果能注入成功就能以src的方式注入外部脚本。

危害

在常用的探测脚本中都是alert(1),弹个框能有啥用,还能上天不?答案是真的能上天:

  • 获取页面数据(你看到的东西别人也能看到,偷取网站数据)
  • 获取Cookies(敏感信息泄露,例如登录态)
  • 劫持前端逻辑(欺骗用户,本来应该做A操作的,结果点击按钮执行了B)
  • 发送请求

因此XSS是比较危险的。

分类

  • 反射型:url参数直接注入
  • 存储型:存储到DB后读取注入

反射型的攻击一般危害略小于存储型,因为URL中带了参数容易被用户察觉。为了让URL中的参数不那么明显,攻击者通常会将URL进行短地址编码,这样用户打开的时候就没有多少疑问了。

如果HTML的属性是用户输入动态构建的,用户就可以通过"进行注入:

<img src = "1" onerror="alert(1)"" />

即用户输入1" onerror="alert(1)"即可完成攻击,这种拼接字符串的方式和SQL注入的一种方式非常类似

常见的XSS攻击存在于HTML属性和HTML内容,因此富文本编辑器中非常容易出现XSS攻击。

防御措施

概括起来就是以下几种:

  • 转义
  • 节点白名单,仅仅保留白名单中的节点
  • CSP

  • 现代浏览器自带拦截,X-XSS-Protection头,可以防止反射型XSS,防止URL中的参数再次出现在HTML内容和属性中,防御非常有限

  • 对输入HTML内容进行转义,将<,>,",'转义为HTML实体即可,参见escape-html的npm模块,其实比较好的方法是对输入的内容进行JSON序列化,js中对应的就是JSON.stringify
  • 对于富文本中XSS攻击的防御比较推荐的做法是按照白名单保留部分标签和属性,因为黑名单实在是太多了(各种script标签,onerror,onmouseover,onclick,每当你增加一个新的黑名单规则很容易找到一个方案绕过!白名单的思想是解析HTML文本构造成文档树(cheerio),仅仅保留白名单中指定的那些类型的节点,如下所示:
function xssFilter(html) {
    const cheerio = require('cheerio');
    const $ = cheerio.load(html);
    const whiteList = {
        'img': ['src'], // img标签只能有src属性
        'font': ['color', 'size'], // font标签只能有color和size
        'a': ['href']
    };
    $('*').each((index, element) => {
        const elementName = element.name;
        if (!whiteList[elementName]) {
            $(element).remove();
            return;
        }
        for (const attr in element.attribs) {
            if (!whiteList[elementName].includes(attr)) {
                $(element).attr(attr, null);
            }
        }
    });
    return $.html();
}

成熟方案js-xss,这个模块采用的就是白名单的思想。

CSP即内容安全策略,这个规范与内容安全有关,主要是用来定义页面可以加载哪些资源,减少 XSS 的发生。XSS之所以是漏洞的原因是“内容被当成了程序”执行了,这样我们只需要将用户输入的内容标记为不可执行,那么注入的内容也不会产生实际的危害。需要服务器设置Content-Security-Policy响应头。

CSRF

跨站请求伪造,注意和XSS中的跨站不同,这个主要是在其他任意网站进行的操作对目标网站造成了影响。一个可怕的例子是:打开了一个别人发给你的网站,你的钱莫名其妙变少了,csrf也被称为one click attack,一点就爆炸。

原理是攻击者获取了用户的登陆凭证仿冒了用户(最常用的攻击方式是携带了cookie)。这种攻击可怕的方式在于被攻击用户是毫不知情的。因此可以用来盗取用户资金,冒充用户进行发帖(灌水的时候可以加上攻击者的网址链接,用户一点就由发起了CSRF攻击,从而造成CSRF蠕虫,影响非常恶劣,例如微博这种UGC分享平台)

防御措施:

  • get请求应该具有幂等性,不能对数据进行修改
  • 禁止第三方网站带cookie,cookie的samesite属性,这种方式防止CSRF非常简单,但是有浏览器兼容性问题。
  • CSRF是绕过了网站的前端直接发送带有cookie信息的请求,因此可以在网站前端加入只有前端才能得到的信息。基于这个思想有2种解决方案:验证码和csrf_token。都能很大程度避免CSRF,但是验证码对体验其实是有降低的,csrf_token的原理是后端生成随机字符串放到页面的表单的隐藏域中(不需要实际显示)和cookie中,表单提交的时候校验cookie中的token和表单中的token是否一致。注意:不管是验证码和csrf_token都需要做空值验证(注意逻辑陷阱,攻击者可能发送空值绕过验证),在ajax请求中token通常放置在meta标签中。
  • 验证refer,禁止来自第三方网站的请求,注意refer是一个完整的url,验证的时候需要保证鲁棒性,例如:http://www.b.com&c=www.a.com,是一个合法refer,如果a.com仅仅用indexOf("www.a.com")进行验证可能有安全问题。
  • 增加验证流程,例如指纹、密码、短信验证码

Cookies的安全问题

Cookies遵循同源策略,只有同源才可以读写。HTTP only的cookie对于js是不可见的,path和domain也可以控制cookie的可见性。

cookie常用来保存用户的登陆凭证,因此直接将uid放在cookie中有巨大的安全隐患(别人可以篡改cookie模仿任意用户),常见的做法是uid+签名。大致流程如下:

const KEY = 'HSD#$#%$6575FDGKJFl';

const crypto = require('crypto');

function signUid(uid) {
    const hmac = crypto.createHmac('sha256', KEY);
    hmac.update(String(uid));
    return hmac.digest('hex');
}

/**
 * @param {*} uid cookie中的uid
 * @param {*} sign cookie中的签名
 */
function checkAuth(uid, sign) {
    const expectedSign = signUid(uid);
    return expectedSign === sign;
}

let uid = 1;
let sign = signUid(uid);
// 登陆成功后向cookie中设置uid和uid签名
console.log('uid = ', uid, 'sign = ', sign);

// 当用户进行其他需要授权的操作的时候验证传入的uid和签名
console.log('合法用户,验证结果', checkAuth(uid, sign));
uid = 2;
console.log('篡改cookie中的uid为2,验证结果', checkAuth(uid, sign));
uid = 1;
sign = 'modifyed sign';
console.log('篡改cookie中的签名,验证结果', checkAuth(uid, sign));
image.png
image.png

除了上述uid+uid签名的方式,有没有不暴露用户信息的方式呢?

答案是有的,将uid以密文的设置到cookie,服务器每次对cookie中的uid进行解密。

另外还有一种基于session的方案,校验用户名和密码之后服务器生成一个键值对,其中key为sessionid(随机字符串),value为用户信息,服务器可以将此信息持久化到外部存储,并将sessionid放入客户端cookie中,客户端每次请求的时候带上cookie,这样服务器就知道当前用户了,这样就可以避免暴露uid。

XSS与cookie的关系

XSS可能盗取用户cookie,设置http only的cookie不会被盗取

CSRF和cookie的关系

csrf则是利用了用户cookie,验证码和csrf token方式有效的原因是无法修改目标网站的cookie,最好是禁用第三方的cookie。

如果没有cookie,csrf和xss的攻击威力至少少一大半。

点击劫持

一个典型的场景是将目标网站放入到iframe中,并将此iframe的透明度设置为0,因此用户实际上是看不到目标网站的,接下来使用一张比较劲爆的背景图吸引用户对用户进行指导按照指定的位置进行点击,这样用户实际上是在和目标网站进行交互。

解决方案是服务器输出header:X-Frame-Options禁止内嵌。例如支付宝进行这样处理的:

<body>
    <h1>sdfds</h1>
    <img src="" alt="">
    <iframe src="http://www.alipay.com" frameborder="0"></iframe>
</body>

当我们试图在自己的网页中内嵌支付宝的时候显示如下:

image.png
image.png

值得注意的是这个HTTP头兼容性非常好(从IE8)

传输安全

http中的connect方法是实现http代理的一种方法,它实际上是一种TCP代理,因此也可以代理HTTPS,但是HTTPS中的内容是密文无法查看。免费的HTTPS常用let’s encrypt

密码安全

足够复杂的密码是对抗彩虹表的有效手段之一,因为彩虹表受限于计算和存储性能。密码的变换次数越多越安全,加密成本几乎不变(生成密码的时候速度慢一些),彩虹表失效(数量太大,无法建立通用性),解密成本增大N倍。

保证密码传输安全主要有以下几种手段:

  1. https传输
  2. 登陆频率限制(一定程度上杜绝暴力破解)
  3. 前端加密意义是有限的(因为前端传的是明文中间人就传明文,前端传的密文中间人就传密文,但是中间人至少无法知道明文密码)

接入层注入问题

SQL注入和XSS非常类似:一部分数据变成了程序。

image.png
image.png
  1. 末尾的1=0使得所有条件为假,不会得到任何数据
  2. 末尾的1=1使得所有条件为真,查出所有数据
  3. 可以用于服务器数据库版本探测,比如如果某个版本有漏洞可能被利用
  4. 针对每一条记录都返回1,2,3
  5. 在4的基础上多了最前面的id列
  6. union可以将结果展示到结果的下方,可以用于探测表有多少个字段(1,2如果不报错则表示有2个字段;1,2,3不报错就表示有3个字段)
  7. 可以对表数据进行探测

SQL注入有非常多的写法,防不胜防,后果非常严重,一定要引起足够重视!其实SQL注入并不是小公司才有,支付宝、腾讯、微博这种体量的公司也是有这种问题的。

SQL注入的防御

  • 关闭错误输出:错误信息对于攻击者有非常大的帮助,黑客可以根据报错决定注入方向是否正确或者向哪个方向改进
  • 检查参数类型:例如输入的id只能是整数,不能拼接其他字符串
  • 对数据进行转义:例如node-mysql包中可以对参数进行转义mysql.escape(param),在使用占位符?的时候会自动调用escape
  • 参数化查询:查询步骤分2步:①将带参数的SQL发给服务器,但是并不真正执行,因为没有真正的参数,这个时候SQL服务器知道了你的意图,②只发送参数。这种方式在Java中叫做PreparedStatement,node-mysql2支持这种方式。使用参数化查询之后用户无法改变SQL语句的意图,最简单最彻底的防御。
  • 使用ORM,安全性好,能提高开发效率

NoSQL的注入和防御

image.png
image.png

可以看到在POST模拟的时候不知道用户名和密码的情况下就得到了token,即成功登陆。后端登陆的伪码如下:

image.png
image.png

主要问题在于password传的不是字符串而是对象,同样的原理我们可以在不知道username的情况下进行相同的注入。

防御方法:

  1. 入参数据检查和转换
  2. 写完整条件

接入层上传问题

上传文件通常分为2步:

  1. 上传文件
  2. 再次访问上传的文件

一般而言上传是不会有问题的,但是当上传的文件被当做程序来解析(例如:服务器是php,上传的是php文件)就有严重问题了。

解决方案有:

  1. 限制后缀名,例如后端是php的话不能上传php文件,有的时候非常简单有效,但是不一定准确
  2. 检查文件类型,这个依赖浏览器的MIME,攻击者可能绕过浏览器
  3. 文件内容检查,不同类型的文件有不同的内容头,即开头的几个字符都是有一定特征的
  4. 程序输出:不要让用户上传的文件被直接访问。即上传文件的目的就是为了显示,可以程序读取文件内容给到前端,但是会降低性能,一个折中的方法是有读文件请求的时候给NGINX发一个指令
  5. 权限控制:写权限和执行权限是互斥的,这一条是核心安全保障,无论在什么情况下都是要遵守的。php文件具有执行权限,但是上传文件的目录有写权限。

部署网站的时候一定要用低权限用户,能满足网站正常需要即可。

OAuth过程

  1. 用户请求使用QQ登陆目标网站
  2. 目标网站重定向到QQ登录页
  3. 用户在QQ登陆页输入自己的用户名和密码
  4. QQ登陆成功后跳转到目标网站
  5. 目标网站获取access_token
  6. QQ服务器返回access_token
  7. 用户在目标网站登陆完成

整个OAuth的过程中用户只是获得了token。这里面有以下几个思想:

  1. 一切行为由用户授权
  2. 授权行为不泄露敏感信息
  3. 授权是会过期的

其实在我们自己的业务中也可以利用Oauth的思想防止资料泄露

image.png
image.png

这样的系统有以下几个特点:

  1. 用户需要授权才能读取资料
  2. 不能批量获取数据
  3. 数据接口可以风控审计(例如1分钟内查了1000个用户的资料可以直接把服务停掉排查)

其他安全问题

DOS

  • TCP半连接
  • HTTP连接
  • DNS:一般带宽很小,攻击DNS是一种非常有效的方案

DDOS是大规模分布式拒绝服务攻击,流量可以达到几十或者上百G,用户来源非常广,很难区分正常用户和攻击者,非常难防御。

更严重的是攻击流量非常大影响到骨干网的时候运营商可能直接把机房流量给你下了,这个时候你的网站自然没人访问了。常见的案例是恶意竞争者攻击别人的游戏服务器。

image.png
image.png

虽然难防,但是我们也是可以做一些有限事情的:

  • 防火墙
  • 交换机,路由器
  • 流量清洗
  • 高防IP(把域名解析到高防IP,背后是大规模的流量清洗服务,能提供非常大的带宽)

开发时候的预防:

  1. 避免重逻辑业务
  2. 快速失败返回(负载越高,越容易失败,重试几率越大,服务越容易崩溃)
  3. 防雪崩机制
  4. 有损服务(保证核心服务可用)
  5. CDN

参考链接