前置知识:
- 原生 JavaScript
- 少量 Node.js 基础
- HTTP 基础知识( Cookies / Session )
- Web 后端基础知识( HTTP / SQL )
- SQL 及 关系型数据库 基础
- 1. 跨站脚本攻击 XSS (Cross Site Scripting)
- 2. 跨站请求伪造 CSRF (Cross Site Request Forgy)
- 3. 前端 Cookies 安全性
- 4. 点击劫持
- 5. 传输过程安全问题
- 6. 用户密码安全问题(接入层)
- 7. SQL注入攻击
- 8. 上传问题(接入层)
- 9. 信息泄露和社会工程学
- 10 安全策略
XSS攻击原理:来自用户的数据被当作脚本在页面中执行
可对用户输入进行转译处理
/** * 对 HTML 节点内容及属性进行 XSS 攻击的预防代码 * 将 HTML 节点内容及属性转译为 HTML 实体 * 适用于绝对禁止用户输入 HTML 内容的场景 */varescapeHTMLProperty=function(str){if(!str)return'';// & 的转译必须放在所有转译的最前面str=str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'&quto;').replace(/'/g,''');// 一般不对空格进行转译// .replace(/ /g, ' ')returnstr;}JavaScript 中可以直接使用 JSON.stringify( str ) 方法进行转译
采用过滤的方式进行防御
/*** 采用 黑名单过滤 对富文本内容进行过滤* 次方法的问题在于 XSS�攻击 的变种很多,很难对所有情况进行逐一过滤* 此处仅做演示,并不会用于真实环境*/varxssFilter=function(html){if(!html)return'';html=html.replace(/<\s*\/?script\s*>/g,'')// 替换 <script> 标签.replace(/javascript:[^'"]*/g,'');// 替换 javascript='' 的调用(常出现在 a 标签).replace(/onerror\s*=\s*['"]?[^'"]*['"]?/g,'')// 替换 <img src=\'abc\' onerror=\'alert(1)\' /> 一类的攻击// 还有 SVG\Object 等 XSS攻击 手段还未进行过滤returnhtml;}白名单过滤需要先解析 HTML。
方法一:这里采用 cheerio 这个库进行解析。(cheerio的使用与 jQuery 类似,比较容易上手)
# 安装 cheerio
npm install cheerio --save-dev/** * 采用 白名单过滤+cheerio 对富文本内容进行过滤 * 需要维护白名单列表,有较好的定制化和灵活性 */varxssFilter=function(html){if(!html)return'';// 白名单列表varwhiteList={'img': ['src'],'font': ['color','size'].'a': ['href'],};// 引入 cheeriovarcheerio=require('cheerio');var$=cheerio.load(html);$('*').each(function(index,e){// 当标签名不在白名单中时,将其移除(name 为标签名)if(whiteList[e.name])$(e).remove();return;// 当标签名在白名单时,对其属性进行判断(attribs 为标签的属性集合)for(varattrine.attribs){// 当属性名不在相应标签的属性列表中时,将其移除// (cheerio 中将属性设为 null 即为移除该属性)if(whiteList[e.name].indexOf(attr)===-1)$(e).attr(attr,null);}});returnhtml;}方法二:使用第三方 XSS 白名单防御库 js-xss
# 安装 js-xss
npm install xss --save-dev/** * 采用 白名单过滤+js-xss 对富文本内容进行过滤 * 快捷、方便,灵活性较差,不需要/少量的白名单维护 */varxssFilter=function(html){if(!html)return'';varxss=require('xss');returnxss(html);}1.3 CSP (Content Security Policy) 内容安全策略
CSRF原理:在用户不知情的倩况下冒充用户身份对目标网站发送请求。
可指定可执行的内容。
<-这只是一个示例,具体的写法以及参数的意义请参见上方的链接-><metahttp-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">CSRF:是一种常见的冒用用户身份的方法
攻击特点:
- B 网站向 A 网站请求
- 请求带 A 网站 Cookies
- 不访问 A 网站前端
- referer 为 B 网站
此方法可有效防止对用户身份( Cookies )的冒用,但仍然可以进行匿名攻击,需要对匿名用户的权限进行控制。(此方法的兼容性不好)
SameSite Cookie,防止 CSRF 攻击
SameSite - OWASP
此方法可防止 CSRF 攻击中不访问被攻击网站前端的问题
- 验证码
ccap 这是一个生成验证码的库。这种方式对用户体验有较大影响,不适宜大量使用。
- token
// 此变量为随机生成并取整,将会放在页面表单及Cookies中varcsrfToken=parseInt(Math.random()*9999999,10);3. 前端 Cookies 安全性
Cookies:前端数据存储
- 前端数据存储(大小:4kb 左右)
- 后端可通过 http 头进行设置
- 请求时通过 http 头传给后端
- 前端可读写
- 遵守同源策略
同源策略:对于 Cookies 而言,同一个源下的 Cookies 才能读写。
源:当协议、域名、端口全部一致时才叫同源。
- 域名:指定 Cookies 的使用范围。
- 有效期:指定 Cookies 的有效期,过期后就会失效。
- 路径:指定 Cookies 作用于网站上的哪一级,具体为 URL 的层级。可以为不同层级的 URL 设置不同的 Cookies,只有当这个层级的页面被访问时对应的 Cookies 才能被访问。
- http-only:Cookies 只能被 HTTP协议 使用,不能被 JavaScript 读取。
- Secure:指定 Cookies 是否只能在 HTTPS 的网站中使用,指定后在 HTTP 的网站中则不能使用。
- SameSite:指定第三方网站的请求是否可以使用 Cookies。(2.1 SameSite Cookie,防止 CSRF 攻击)
浏览器开发者工具中查看 Cookies 信息: 在 浏览器的开发者工具 -> Application/存储空间 -> Cookies 就可以找到 Cookies 信息。
- Name(名称): 健
- Value(值): 值
- Domain(域): 域名
- Path(路径): 路径
- Expires / Max-Age(过期时间): 有效期(Session 表示只在会话内有效)
- Size(大小): 大小
- HTTP(HTTP): http-only
- Secure(安全): Secure
- SameSite(相同站点): SameSite(不是所有浏览器都有此属性)
英文名称参考Google Chrome 70.0.3538.102(正式版本)(64 位),中文名称参考Safari 浏览器12.0.1 (14606.2.104.1.1)
- 读取 Cookies:document.cookie
- 设置 Cookies 有效期:document.cookie = 'a=1;expires=Tue, 27 Nov 2018 03:40:29 GMT'
- 删除 Cookies:并没有删除 Cookies 的方法,但可以通过给 Cookies 的有效期设置一个过去的时间,就可以删除 Cookies。
expires 的值为过期时间,且只能为此格式,此格式时间的快速获取方法:
vard=newDate();d.toGMTString();
- 存储个性化设置
- 存储未登录时用户的唯一标识
- 存储已登陆用户的凭证
- 存储其他业务数据(页面缓存)
Cookies - 登陆用户凭证
- 前端提交用户信息
- 后端验证用户信息
- 后端通过 HTTP 头设置用户凭证
- 后续访问时后端先验证用户凭证
- 用户ID,使用用户ID作为用户的标示并不安全,有被篡改的风险
- 用户ID + 签名:可防止 用户ID 被篡改,Cookies 中会同时存储用户ID和签名,作为校验。
// 加密签名模块// crypto 为 Node�Js 自带的加密模块importcryptofrom'crypto';varcrypt={};// 随机的字符,越复杂越安全constkey='#@56562366&##%';crypt.cryptUserId=function(userId){varsign=crypto.createHmac('sha256',key)sing.update(userId+'');returnsign.digest('hex');}exportcrypt;- SessionId:在 Cookies 中不存储任何用户信息,后端可通过 SessionId 判断用户的身份
/** * SessionId*/varsession={};varcache={};session.set=function(userId,obj){varsessionId=Math.random();if(!cache[sessionId]){cache[sessionId]={};}cache[sessionId].content=obj;returnsessionId;}session.get=function(userId){returncache[sessionId]&&cache[sessionId].content;}exportsession;- XSS 可能偷取 Cookies
- http-only 的 Cookies 不会被偷(3.2 Cookies 的特性)
- CSRF 利用了用户 Cookies
- 攻击站点无法读写 Cookies(2.2 在前端页面加入验证信息)
- 最好能阻止第三方使用 Cookies(2.1 SameSite Cookie,防止 CSRF 攻击)
- 签名防篡改(3.5.1 生成用户凭证)
- 私有变换(加密)
// node 中的加密importcryptofrom'crypto';// 密钥varkey='22e323##%&@%#$565';// 越复杂,越安全// 加密// 创建加密对象varcipher=crypto.createCipher('des',key);cipher.update('hello world','utf8','hex');text+=cipher.final('hex');// 解密// 创建解密对象vardecipher=crypto.createDecipher('des',key);decipher.update(text,'hex','utf8');originalText+=decipher.final('utf8');// 加密和解密过程均为流式输出,因此要采用 += 接收,否则只会有部分内容。- heep-only(防止XSS)(3.2 Cookies 的特性)
- secure(防止传输过程中的窃听)(3.2 Cookies 的特性)
- SameSite(2.1 SameSite Cookie,防止 CSRF 攻击)
点击劫持:是一种常见的利用用户的身份,在用户不知情的情况下完成操作的一种攻击
点击劫持的特点:
- 用户操作,但不知情
- 通过点击能进行的操作都可以通过点击劫持进行
将目标网站作为一个 iframe 嵌入到攻击者网站中,并在视觉上进行隐藏(如调整透明度),用户看不见这个 iframe。对用户的点击进行引导,使用户进行一些指定的操作(如某种游戏),从而实现特定的目的(如银行转账)。
目标网站能够被攻击网站嵌套在 iframe 中。
未内嵌的网站与内嵌网站的区别在于,未内嵌的网站的 top === window 和 top.location === window.location 均为 true。而在 iframe 中 top 指向最外层的 window,window 指向 iframe 本身,结果为 false。
- JavaScript 禁止内嵌
/** * JavaScript 禁止内嵌* 此方法的问题在于 HTML5 iframe 中有一个新的属性* [sandbox](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/iframe)* sandbox='allow-forms' 时可以正常提交表单,但不会执行脚本,导致此方法失效*/if(top.location!==window.location){top.location=window.location;}X-FRAME-OPTIONS 禁止内嵌(推荐)
辅助手段 2.2 在前端页面加入验证信息
IFrame 安全策略(允许内嵌)
HTTP 传输窃听和篡改:由于 HTTP 采用明文传输信息,导致请求发送过程中的所有设备(浏览器、代理服务器、链路、服务器等)均可读取甚至篡改发送的数据。
# 查看向一个 url 发送请求时会经过那些中间节点# 其中所展示的任何一个节点均可进行 HTTP 传输窃听和篡改# Mac OS/Linux
traceroute url
# Windows
tracert url- 窃听通过网络传输的一切信息,包括敏感信息(账号、密码等)
- 插入广告
- 重定向网站(如钓鱼网站)
- 无法防御的 XSS 和 CSRF 攻击
确认服务器身份的原理:
- 浏览器 -> CA 获取内置信任列表
- 服务器 -> CA 申请证书
- CA -> 服务器 验证域名,颁发证书
- 浏览器 -> 服务器 发起请求
- 服务器 -> 浏览器 出具证书
- 浏览器 验证通过
CA 安全的原则:
- 证书无法伪造
- 这书私钥不能泄露
- 域名管理权不能泄露
- CA 遵守原则
浏览器开发者工具中查看 这书 信息: 在 浏览器的开发者工具 -> Security 点击 View Certificate 就可以查看 这书 信息。
Mac OS 中查看本机中受信任的证书: 打开 钥匙串访问(keycha)程序 -> 系统跟证书 就可以查看系统中所有受信任的证书
证明用户身份:将存储的密码与输入的密码进行比对以确认用户身份。
密码的泄露渠道
- 数据库被盗
- 服务器被入侵
- 通许被窃听
- 内部人员泄露数据
- 其他网站(撞库):多网站采用相同的账号密码,一个被盗殃及其他
- 严禁明文存储(防泄漏)
- 单向变换(防泄漏)
- 变换复杂度要求(防猜解)
- 密码复杂度要求(防猜解)
- 加盐(防猜解)
- 哈希算法(信息摘要)
单向变换彩虹表 会导致简单的密码及加密变得不安全。由于彩虹表的计算及存储限制,使用复杂的密码(长度 + 复杂度)即可大幅减少密码被反推的威胁。
- 帮助用户加强复杂度:
- 多级加密(防止彩虹表反推)
- 增加密码长度:在用户密码的基础上增加固定的字符(防止彩虹表反推)
- 加盐:增加用户唯一的字符(提高安全性)
多级(嵌套)加密的好处:
- 变换越多越安全
- 加密成本几乎不变(密码的生成会慢一些)
- 彩虹表失效(密码足够复杂)
- 解密成本倍增(更安全)
/*** 密码加密* 这里只做了两次加密,不是很安全* 实际项目中可使用多协议多次加密,可提高安全性*/importcryptfrom'crypto';letpassword={};constmd5=(str)=>{constmd5Hash=crypt.createHash('md5');md5Hash.update(str);returnmd5Hash.digest('hex');}constsalt=()=>{// 这里采用硬编码添加字符串,字符串一旦添加不能改变且越复杂越安全returnmd5(Math.random()+999999+'fd4fb5 e#687f%$@_%%$#32'+newDate().getTime());}password.encryptPassword=(password)=>{returnmd5(salt+password);}exportpassword;此处不做说明
添加 js-md5 模块
npm install -save-dev js-md5importmd5from'js-md5';// 此处只是示例,可增加加密字符串的复杂度以提高安全性constpassword=md5(password)- 生物特征密码
- 指纹
- 声纹
- 虹膜
- 人脸识别
生物特征密码的问题:
- 私密性 - 容易泄露
- 安全性 - 碰撞(生物识别采用相似性进行匹配,并不能保证完全匹配)
- 唯一性 - 终身唯一,无法改变(一旦被破解很难改变)
- 猜解密码
- 获取数据
- 删库删表
- 拖库
- 关闭错误输出(不要将错误信息抛到前台,应做模糊处理)
- 检查数据类型(对传入 SQL 的数据做类型检查,包括用户输入的和层序获取的,以防篡改)
- 对数据进行转译(转译不能防止所有的 SQL 注入)
constmysql=require('mysql');exports.getConnection=function(){/** * 此为 Mysql 对象 */letconnection=mysql.createConnection({host: 'localhost',database: 'safety',user: 'root',password: '123456789'});connection.connect();returnconnection;};// 参数转译方法connection.escape(id);// 一、转译 SQL 语句`select * from table where id = ${connection.escape(id)}`// 二、通过类似参数化查询,在传递参数前会进行转译`select * from table where id ?`,[id]- 使用参数化查询(避免 SQL 注入威胁)
mysql2 可以支持 MySQL 参数化查询,从而避免 SQL 注入的威胁。mysql2 为第三方工具,向下兼容 mysql
- 使用 ORM(对象关系映射)
Sequelize | Github :是一个Node.js ORM, 目前支持 Postgres, MySQL, SQLite 和 Microsoft SQL Server.
安装 Sequelize
# 安装 Sequelize
npm install --save-dev Sequelize
# 还有以下之一:
$ npm install --save pg pg-hstore // Postgres
$ npm install --save mysql2 // MySQL
$ npm install --save sqlite3 // SQLite
$ npm install --save tedious // MSSQL/** * sequelize.js * Sequelize 实例 * 此处引入的为刚才安装的 sequelize 第三方库 */importSequelizefrom'sequelize';letsequelize=newSequelize({host: 'localhost',database: 'safety',username: 'root',define: {freezeTableName: true,},});exportsequelize;// -------------------------------------------/** * Post.js * 使用 Sequelize 定义的数据模型 * 此处引入的为刚才创建的 sequelize 实例sequelize.js, * 此处一定注意区分 */importsequelizefrom'./sequelize';// 实例importSequelizefrom'sequelize';// �模块varPost=sequelize.define('post',{// 字段},{// 此处可不加// 如果添加 则为指定表名// 如果不加 sequelize 会尝试对应表名tableName: 'post'.});exportPost;- 文件由用户上传
- 用户能够通过 Url 访问上传的文件
- 访问时文件可能会当作程序执行
/*** 禁止用户上传 JS 文件(有时此方法会不可靠)* 此处只做演示,在 Nodejs 环境中几乎没有上传漏洞* 但在如 PHP 中,上传漏洞较为严重* 不同语言的文件处理不同,需要不同的防御方式*/importpathfrom'path';// file 为上传的文件对象// 此处为取文件后缀名constext=path.extname(file.name);if(ext==='js')thrownewError('不能上传js文件');/*** 文件类型检查(此方法为浏览器提供,可绕过)* 不同语言的文件处理不同,需要不同的防御方式*/if(file.type!=='image/png')thrownewError('上传的文件类型错误');/** * 文件内容检查(可欺骗)* 由于同一类型文件的前几位二进制位是相同的,可以根据此特性进行判断* 但可通过修改文件前几位让层序误判文件类型*/importfsfrom'fs';constfileBuffer=fs.readFileSync(file.path);fileBuffer[0]==ox5b;在访问莫文件时不直接输出,而是通过程序读取文件再输出,从而阻止了文件的执行。Node.js 的 fs 模块就是采用的这种方式,因此在 Node.js 中几乎不存在上传文件漏洞,在其他环境中可以使用此方法进行防御。此方法会有一定的性能损失。
importfsfrom'fs';fs.readFileSync(filePath);对目录文件权限进行控制,可写(如用户上传的文件)目录中的不给予执行权限,可执行(如程序文件)目录不给予写入权限,防止攻击中篡改和注入。
- 泄露系统的敏感信息
- 泄露用户的敏感信息
- SQL注入
- XSS / CSRF
- 错误信息失控:将错误信息直接暴露
- 水平权限控制不当:系统中用户权限管理不当
- 一切行为由用户授权
- 授权行为不泄露敏感信息(请求授权的网站只能拿到 token,根据此 token 确认用户身份)
- 授权会过期
使用 OAuth 思想 防止数据泄露
- 用户授权读取资料
- 无授权资料不可读取
- 不允许批量获取数据
- 数据接口可风控审计