web 安全主要分以下几个层面

  • 代码层面
  • 架构层面
  • 运维层面

本来主要围绕架构层面进行展开,如何写一个没有漏洞的网站。了解安全问题的原因,避免开发带漏洞的代码,如何开发安全的 web 项目。具体会遇到以下的安全问题,我们以此来研究及如何防御

安全问题

  • 用户身份被盗用
  • 用户密码泄露
  • 用户资料被盗取
  • 网站数据库泄露
  • 其它

主要攻击手段分为以下几种

  • 跨站脚本攻击 xss(cross site scripting)
  • 跨站请求伪造攻击 CSRF(cross site request forgy)
  • 前端 cookie 安全性
  • 点击劫持攻击
  • 传输过程安全问题
  • 用户密码安全问题
  • SQL 注入攻击
  • 信息泄露和社会工程学
  • 其它安全问题

1. 跨站脚本攻击 xss

跨站脚本(cross site script)为了避免与样式 css 混淆,所以简称为 XSS。

XSS 是一种经常出现在 web 应用中的计算机安全漏洞,也是 web 中最主流的攻击方式。那么什么是 XSS 呢?

XSS 是指恶意攻击者利用网站没有对用户提交数据进行转义处理或者过滤不足的缺点,进而添加一些代码,嵌入到 web 页面中去。使别的用户访问都会执行相应的嵌入代码。从而盗取用户资料、利用用户身份进行某种动作或者对访问者进行病毒侵害的一种攻击方式。

攻击原理:

客户端提交的数据本来就是应用所需要的,但是恶意攻击者利用网站对客户端提交数据的信任,在数据中插入一些符号以及 javascript 代码,那么这些数据将会成为应用代码中的一部分了。那么攻击者就可以肆无忌惮地展开攻击。

攻击危害:

  • 获取页面数据 - 偷取网站任意数据
  • 获取 cookies - 偷取用户资料
  • 劫持前端逻辑 - 偷取用户密码和登录态
  • 发送请求 - 欺骗用户

1.1 XSS 攻击分类:

主要分为两大类

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

了解即可,不必细究,XSS 根源就是没完全过滤客户端提交的数据

1.1.1 反射型 xss 攻击

又称为非持久性跨站点脚本攻击,它是最常见的类型的 XSS。漏洞产生的原因是攻击者注入的数据反映在响应中。一个典型的非持久性 XSS 包含一个带 XSS 攻击向量的链接(即每次攻击需要用户的点击)。

正常发送消息:

http://www.test.com/message.php?send=Hello,World!

接收者将会接收信息并显示Hello,Word

非正常发送消息:

http://www.test.com/message.php?send=<script>alert(‘danger!’)</script>!

接收者接收消息显示的时候将会弹出警告窗口

1.1.2 存贮型 xss 攻击

又称为持久型跨站点脚本,它一般发生在 XSS 攻击向量(一般指 XSS 攻击代码)存储在网站数据库,当一个页面被用户打开的时候执行。每当用户打开浏览器,脚本执行。持久的 XSS 相比非持久性 XSS 攻击危害性更大,因为每当用户打开页面,查看内容时脚本将自动执行。谷歌的 orkut 曾经就遭受到 XSS。

从名字就可了解到存储型 XSS 攻击就是将攻击代码存入数据库中,然后客户端打开时就执行这些攻击代码。例如留言板

留言板表单中的表单域:<input type=“text” name=“content” value=“这里是用户填写的数据”>

正常操作:

用户是提交相应留言信息;将数据存储到数据库;其他用户访问留言板,应用去数据并显示。

非正常操作:

攻击者在value填写<script>alert(‘attach!’)</script>【或者html其他标签(破坏样式。。。)、一段攻击型代码】;

将数据存储到数据库中;

其他用户取出数据显示的时候,将会执行这些攻击性代码

XSS 攻击注入点:

  • HTML 节点内容
<div><script type="text/javascript">alert(1)</script></div>
  • HTML 属性
<img src="1" onerror="alert(1)" alt="">
  • JavasSript 代码
<script type="text/javascript">var data="##{data}";var data="hello";alert(1);"";</script>
  • 富文本
  • 得保留 HTML
  • HTML 有 XSS 攻击风险

1.2 防御 XSS

  • 浏览器自带防御
  • html 转义(针对 html 标签和属性)
  • js 脚本转换
  • 富文本过滤(一般在输入的时候)
  • CSP(content security policy)- 最好解决方案

1.2.1 浏览器自带防御

只针对反射型(html 内容或 html 属性),并不是所有浏览器都支持

1.2.2 html 转义

var escapeHtml = function(str) {
  if (!str) return '';
  str.replace(/&/g, '&amp;'); //只能在最前面,会对下面做转义
  str.replace(/</g, '&lt;'); //<>html标签,引号html属性
  str.replace(/>/g, '&gt;');
  str = str.replace(/"/g, '&quto;');
  str = str.replace(/'/g, '&##39;');
  //str = str.replace(/ /g, '&##32;'); //空格可能有问题,对html属性最好加引号
  return str;
};

1.2.3 js 脚本转换

<script type="text/javascript">
  var data="##{data}";
  var data="hello";alert(1);"";
</script>
var escapeForJs = function(str) {
  if(!str) return '';
  str = str.replace(/\\/g, '\\\\')
  str = str.replace(/"/g, '\\"')
}

更好的方式是使用 JSON.stringify 转换

1.2.4 富文本过滤

  • 按黑名单过滤
var xssFilter = function(html) {
  if (!html) return '';
  html = html.replace(/<\s*\/?script\s*>/g, '');
  html = html.replace(/javascript:[^'"]*/g, '');
  html = html.replace(/onerror\s*=\s*['"]?[^'"]*['"]?/g, '');
  return html;
};
// 标签太多,属性太多,不适合此办法
  • 按白名单保留部分标签和属性
// cheerio库

var xssFilter = function(html) {
  if (!html) return '';
  var cheerio = require('cheerio');
  var $ = cheerio.load(html);

  // 白名单
  var whiteList = {
    img: ['src'],
    font: ['color', 'size'],
    a: ['href']
  };
  $('*').each(function(index, elem) {
    if (!whiteList[elem.name]) {
      $(elem).remove();
      return;
    }

    for (var attr in elem.attribs) {
      if (whiteList[elem.name].indexOf(attr) === -1) {
        $(elem).attr(attr, null);
      }
    }
  });
  console.log(html, $.html());
  return $.html();
};
// 第三方库js-xss(根据白名单过滤HTML)
// npm install xss
var xssFilter = function(html) {
  if (!html) return '';
  var xss = require('xss');
  var ret = xss(html);

  console.log(html, ret);
  return ret;
};

1.2.5 CSP 内容安全策略(推荐)

# 指定哪些内容可执行
child-src
connect-src
default-src
font-src
frame-src
img-src
manifest-src
media-src
object-src
script-src
style-src
worker-src
# 指定可信任来源

 # 主机

 'self' # 协议,self指同域
                'unsafe-inline' # 直接插入页面的内容
                'unsafe-eval' # eval函数
                'none' # 不信任任何内容
                'nonce-' # 一次凭证,指定内容与页面匹配才信任
                 # 哈希
                'strict-dynamic' # 信任脚本内部引入的脚本
# 格式
# 设置头信息
Content-Security-Policy: default-src "self" http://www.example.com;
                         content-src "none";

2. 跨站请求伪造攻击 CSRF(cross site request forgy)

跟 XSS 攻击一样,存在巨大的危害性,攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作。

CSRF 攻击原理:

第三方网站通过访问主网站后端带上 cookie 信息进行用户伪造


CSRF 危害:

  • 利用用户登录态
  • 用户不知情
  • 完成业务请求
  • 盗取用户资金
  • 冒充用户发帖背锅
  • 损坏网站名誉

2.1 CSRF 防御

  • 禁止第三方网站带 Cookies
    • same-site 属性(同一站点)-但是只有 chrome 是完全可以
  • 在前端页面加入验证信息(ccap 模块生成验证码-npm install ccap –save)
    • 验证码(攻击者网站不访问前端就没法拿到验证码通不过验证)
    • token
// 将token放入表单及cookie中,然后校验两者是否一致
var csrfToken = parseInt(Math.random() * 9999999, 10);
ctx.cookies.set('csrfToken', csrfToken);
  • 验证 referer
// 禁止来自第三方网站的请求
var referer = ctx.request.headers.referer;
if (!/^https?:\/\/localhost/.test(referer)) {
  throw new Error('非法请求');
}
  • 前端数据存储
  • 后端通过 http 头设置
  • 请求时通过 http 头传给后端
  • 前端可读写
  • 遵守同源策略(协议,域名,端口)

cookie 特性:

  • 域名
  • 有效期
  • 路径
  • http-only
  • secure(https)
  • samesite

cookie 作用:

  • 存储个性化设置
  • 存储未登录时用户唯一标识
  • 存储已登录用户的凭证
    • 前端提交用户名和密码
    • 后端验证用户名和密码
    • 后端通过 http 头设置用户凭证
    • 后续访问时后盾先验证用户凭证
  • 存储其它业务数据

3.1.1 用户 ID(隐患)

3.1.2 用户 ID + 签名

// crypt.js
var crypt = {};
const KEY = '##ksdfidf312##!@%dj1';
crypt.cryptUserId = function(userId) {
  var crypto = require('crypto');
  var sign = (crypto.createHmac = ('sha256', KEY));
  sign.update(userId + '');
  return sign.digest('hex');
};
module.exports = crypt;

// 登录模块
ctx.cookies.set('sign', crypt.cryptUserId(user.id), {
  httpOnly: false,
  sameSite: 'strict'
});
ctx.cookies.set('userId', crypt.cryptUserId(user.id), {
  httpOnly: false,
  sameSite: 'strict'
});

// 验证
const crypt = require('./crypt');

var userId = ctx.cookies.get('userId');
var sign = ctx.cookies.get('sign');
var correctSign = cryptUserId(userId);
if (correctSign !== sign) {
  throw new Error('err');
}

3.1.3 SessionId(持久化)

// session.js

var session = {};
var cache = {};
session.set = function(userId, obj) {
  var sessionId = Math.random();
  if (!cache[sessionId]) {
    cache[sessionId] = {};
  }
  cache[sessionId].content = obj;
  return sessionId;
};

session.get = function(sessionId) {
  return cache[sessionId] && cache[sessionId].content;
};
module.exports = session;

// user.js
const session = require('./session');

var sessionId = session.set(user.id, {
  userId: user.id
});
ctx.cookies.set('sessionId', sessionId, {
  httpOnly: true,
  sameSite: 'strict'
});

// site.js
const session = require('./session');

var sessionId = ctx.cookies.get('sessionId');
var sessionObj = session.get(sessionId);
if (!sessionObj || !sessionObj.userId) {
  throw new Error('session不存在');
}
var userId = sessionObj.userId;

3.2 cookies 和 xss 关系

  • xss 可能偷取 cookies
  • http-only 的 cookie 不会被偷

3.3 cookies 和 csrf 关系

  • csrf 利用了用户 cookies
  • 攻击站点无法读写 cookies
  • 最好能阻止第三方使用 cookies

3.4 cookies - 安全策略

  • 签名防篡改
  • 私有变换(加密)- 信息隐藏
  • http-only(防止 xss)
  • secure(https)
  • same-site(主要 csrf 攻击,兼容性不好)

3.4.2 私有变换(加密)

// 加密(node加密模块)
var crypto = require('crypto');
var KEY = '9851##%*!df1%!';
var cipher = crypto.createCipher('des', KEY);
var text = cipher.update('hello world', 'utf8', 'hex'); // 一边加密一边输出
text += cipher.final('hex');
console.log(text);

// 解密
var decipher = crypto.createDecipher('des', KEY);
var originalText = decipher.update(text, 'hex', 'utf8');
originalText += decipher.final('utf8');
console.log(originalText);

签名和加密是有区别的。签名的文件仍然是明文,内容公开,签名可以验证是否是用户传达的文件。加密的文件是密文,用户无法获取。

4. 点击劫持攻击

原理:

通过 iframe 将目标网站放到页面上,将透明度设为 0,直接点击的是 iframe;

特征:

  • 用户亲手操作
  • 用户不知情

4.1 点击劫持防御

  • javascript 禁止内嵌(有局限性)
  • X-FRAME-OPTIONS 禁止内嵌(ie8 以上)(推荐)
  • 其他辅助手段
    • 验证码

4.1.1 javascript 禁止内嵌

html5 新属性sandbox,什么都不加默认会禁用掉很多功能

<!-- 允许表单提交 -->
<iframe src="http://xxxx.cn:1211/post/15" sandbox="allow-forms"></iframe>
<!-- sandbox禁用功能 -->

这种方式有局限性,sandbox 即使加上有时候也会存在劫持。

<script type="text/javascript">
  if(top.location != window.location){
    top.location = window.location;
  }
</script>

4.1.2 X-FRAME-OPTIONS 禁止内嵌

ctx.set('X-Frame-Options', 'DENY');

DENY; // 禁止内嵌
SAME - ORIGIN; // 同一网站
SAMEORIGIN; // 嵌入页和被嵌入页在用一个域
ALLOW - FROM; // 网址

5. 传输过程安全问题

  • 传输链路窃听篡改
  • https

5.1 传输链路窃听篡改

我们可以用 anyproxy 代理软件来模拟攻击

  • http 窃听
    • 窃听用户密码
    • 窃听传输敏感信息
    • 非法获取个人信息
  • http 篡改
    • 插入广告
    • 重定向网站
    • 无法防御 xss 和 csrf 攻击

5.2 https 防御

超文本传输安全协议( HTTPS )是一种通过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。 HTTPS 开发的主要目的,是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。

还有一种情况是中间人攻击,看下图:

这种情况下要防御就需要引入证书

  • 证书无法伪造
  • 证书私钥不被泄露
  • 域名管理权不泄露
  • CA 坚守原则

免费证书 www.sslforfree.com

5.2.1 https 部署

curl https://get.acme.sh | sh
/root/.acme.sh // 安装目录
cd /root/
./acme.sh --issue -d www.example.com --webroot /data/web/www.example.com/

然后配置 nginx

server{
  listen 80;
  listen 443 ssl http2;
  server_name www.example.com;

  ssl_certificate /root/.acme.sh/www.example.com/fullchain.cer;
  ssl_certificate_key /root/.acme.sh/www.example.com/www.example.com.key;

  location / {
    root /data/web/www.example.com;
  }
}

6. 密码安全问题

  • 密码作用
  • 密码存储
  • 密码传输安全性
  • 密码的替代方案
  • 生物特征密码问题

密码 - 泄露渠道

  • 数据库被偷
  • 服务器被入侵
  • 通讯被窃听
  • 内部人员泄露
  • 其他网站(撞库)

6.1 密码存储

  • 严禁明文存储(防泄露)
  • 单向变换(防泄露)
  • 变换复杂度要求(防猜解) - 变换次数越多越安全
    • 加密成本几乎不变(生成密码时速度慢一些)
    • 彩虹表失效(数量太大,无法建立通用性)
    • 解密成本增大 N 倍
  • 密码复杂度要求(防猜解)
  • 加盐(防猜解) - 帮助用户提高复杂度
    • md5(sha1(md5(ID+ab83kd+原始密码+81kdso+盐+1lso;$2)))

6.1.1 严禁明文存储

正常在数据库中是需要离线加密明文密码的,这里为了方便,我们在用户登录时做逻辑处理(加密、加盐)

// user.js
const password =  require('./password')

const results = await query(
  `select * from user where username = '${data.username}'`
)
if(results.length){
  let user = results[0];
  //如果用户没有salt
  //需要升级
  if(!user.salt){
    var salt = password.getSalt();
    var newPassword = password.encryptPassword(salt, user.password);
    await query(`update user set password = '${newPassword}', salt = '${salt}' where id = ${user.id}`);
    user.salt = salt;
    user.password = newPassword;
  }
  var encryptPassword = password.encryptPassword(user.salt, data.password)
  if(encryptPassword !== user.password){
    throw new Error('密码不正确');
  }
}
// password.js
var password = {};
var md5 = function(str) {
  var crypto = require('crypto');
  var md5Hash = crypto.createHash('md5');
  md5Hash.update(str);
  return md5Hash.digest('hex');
};
password.getSalt = function() {
  return md5(Math.random() * 999999 + '' + new Date().getTime());
};
password.encryptPassword = function(salt, password) {
  return md5(salt + 'af$!##@$14##svc*&sf%' + password);
};

module.exports = password;

6.1.2 单向变换

  • 哈希算法
    • 明文-密文 – 对应
    • 雪崩效应(明文有一点点不一样密文完全不一样)
    • 密文 - 明文 无法反推
    • 密文固定长度
    • 常见哈希算法:md5 sha1 sha256

虽然没有办法反推出原密码,但是可以查表,网上有很多这种彩虹表的查询

这种情况下可以组合加密来防止彩虹表的查询

6.2 密码传输安全性

  • https 传输
  • 频率限制
  • 前端加密意义有限

前端加密

jspm install js-md5

7. SQL 注入攻击

关系型数据库

  • 存放结构化数据
  • 可高效操作大量数据
  • 方便处理数据之间的关联关系
  • 常见:access/sqlite/mysql/mssql server

注入攻击危害

  • 猜解密码
  • 获取数据
  • 删库删表
  • 拖库

sql 语句

select * from user where id=1;

insert into user(username,password) values('test','123');

update user set password='1234' where id=3;

delete from user where id=3;

7.1 sql 注入攻击案例

select * from user where username = 'TooBug' and password = '1' or '1'='1'; # 否定前面语句

select * from table where id="10" and mid(version(),1,1)=5 # 截取版本号第一位是否等于5,等于5前面语句生效,否则报错。

select id,1,2,3 from table;

select * from table union select 1,2,3 from table2; # 联合查询

select * from table where mid(username,1,1)="t" # 查询username第一位等于't';

7.2 sql 注入攻击防御

  • 关闭错误输出
  • 检查数据类型
  • 对数据进行转义
  • 使用参数化查询(抓包 charles,wireshark)
  • 使用 ORM(对象关系映射)- 增强安全性,开发效率

7.3 NoSQL 注入攻击和防御

  • 检查数据类型
  • 类型转换
  • 写完整条件

8. 接入层上传问题

8.1 上传攻击

  • 上传文件
  • 再次访问上传的文件
  • 上传的文件被当成程序解析

8.2 防御

  • 限制上传后缀(比如 php 程序禁止上传 php 文件)
  • 文件类型检查
    • fileType
  • 文件内容检查(开头特征)
var fileBuffer = fs.readFileSync(file.path);
fileBuffer[0] == 0x5b;
  • 程序输出(node 就是如此,但是因为要读到内存中,有读和写,性能受影响。折中办法:访问文件直接输出指令给到前端 nginx 服务器读取,性能损耗会小一些)
  • 权限控制 - 可写可执行互斥(原则)
    • 上传文件可写不可执行
  • 部署应用时用低权限用户,而不用 root 这种高权限用户

9. 信息泄露和社会工程学

  • 信息泄露
    • 泄露系统敏感信息
    • 泄露用户敏感信息
    • 泄露用户密码
  • 信息泄露途径
    • 错误信息失控(mysql)
    • sql 注入
    • 水平权限控制不当
    • xss/csrf
  • 社会工程学
    • 你的身份由你掌握的资料确定
    • 别人掌握了你的资料
    • 别人伪装成你的身份
    • 利用你的身份干坏事

案例:

  • 电信诈骗
  • 伪装公检法
  • QQ 视频借钱
  • 微信伪装好友

9.1 OAuth 思想

  • 一切行为由用户授权
  • 授权行为不泄露敏感信息
  • 授权会过期

9.1.1 利用 OAuth 思想防止资料泄露

  • 用户授权读取资料
  • 无授权的资料不可读取
  • 不允许批量获取数据
  • 数据接口可风控审计

10. 其它安全问题

  • 拒绝服务攻击 DDOS
  • 重放攻击

10.1 拒绝服务攻击 DOS

特征:

  • 模拟正常用户
  • 大量占用服务器资源
  • 无法服务正常用户

类型:

  • TCP 半连接
  • HTTP 连接
  • DNS(域名解析服务器)

10.1.1 大规模分布式拒绝服务攻击 DDOS

  • 流量可达几十到上百 G
  • 分布式(肉鸡、代理)
  • 极难防御

10.1.2 DOS 攻击防御

  • 防火墙
  • 交换机、路由器
  • 流量清洗
  • 高防 IP

10.1.3 DOS 攻击预防

  • 避免重逻辑业务
  • 快速失败快速返回
  • 防雪崩机制
  • 有损服务
  • CDN

10.2 重放攻击

原理:

  • 请求被窃听或记录
  • 再次发起相同的请求
  • 产生以外的结果

后果:

  • 用户被多次消费
  • 用户登录态被盗取
  • 多次抽奖

10.2.1 防御

  • 加密(https)
  • 时间戳
  • token(session 后端生成)
  • nonce
  • 签名

总结

cookies 和 session 关系

session 是基于 cookies 的存储来实现的(常用)

cookies 特性

容量小,跟随 html 发送,通过 http 头设置,通过 js 读取

cookies 属性

  • 域名,有域名不能跨域
  • 路径,同一域名 path 不一样 cookies 读取情况不一样
  • 有效期
  • http-only,没办法通过 js 读取,通过 http 发送后台给后台读取
  • same-site

删除一个 cookis 值?

设置一个过期时间为过去时间。

https 如何保证数据不被窃听的

通过数据加密方式,在客户端和服务端进行通信之前来协商一个加密协议,通过加密协议来传输所有数据。

https 如何保证不被中间人攻击

证书机制

部署 https 的步骤

  • 确保全栈都可以部署到 https
  • 找 ca 生成证书