赞
踩
[TOC]
漏洞场景
输入错误的用户名,会有明确的提示——帐号不存在。
攻击者可以通过此方法枚举找出存在的帐号,存在安全漏洞
整改建议:帐号密码校验时,无论账号还是账号密码错误统一给以模糊的提示如:"帐号或密码错误"即可。
漏洞描述:在static下存放的不经过webpack打包的文件,页面会直接请求获取,从浏览器的Network中可以看到文件的请求地址和内容,如果其中有关键的信息,就会存在泄露问题。
如: http://xxxxxx/static/configUrl.js 直接获取到前端的静态资源文件中有的关键信息
整改建议:需要对敏感的信息如上面的内部ip地址进行加密、隐藏或不存放在此处配置文件中
漏洞描述: 永远不要相信用户输入的信息,如常规的注入脚本通过input输入之后被页面执行
整改办法
方法1:对于vue项目中ElementUI的el-input 和 原生input
<el-input :placeholder="privateSearchPlaceholder" v-model.trim="privateValue" @keyup.enter.native="querySearchStr" clearable maxlength="100" @clear="clearSearchStr"></el-input>
watch: {/** * 监听文本框值变化 * */privateValue (val) {// 此处不加nextTick,显示的还会使原来的带有特殊字符的字符串;但是实际的值已经改变this.$nextTick(() => {// 去除特殊字符(除去数字字母中文之外都是特殊字符)this.privateValue = val.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]+/g, '') // this.privateValue =this.$myTools.stringFilter(val) // 也可全局注册过滤函数})}
}
方法2:针对ElementUI的el-input
<el-input @input="onInput" v-model.trim="searchStr"></el-input>
methods:{onInput(val){console.log(val)this.searchStr = e.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]+/g, '')}
}
注意:前端禁止了特殊字符,但是抓包工具直接请求还是可以发送特殊字符,所以建议后端关键接口也要对特殊字符做处理,SQL注入也可用类似方法解决部分(当然如果已经做了防重放的话也可以理论上避免抓包重发)。
漏洞描述:涉及到用户密码的系统,需要限制用户对账号密码的复杂度做要求,不可允许使用弱密码
整改建议:配置安全合理的密码策略,如必须为包含数字、字母、特殊字符,且长度不可短于8位,下面是对复杂度做验证的函数
密码长度8~30位,且必须包含字母、数字、特殊字符的vue表单校验
let pwd = '';
var pwdValidate = function(rule, value, callback){pwd = valueif(!value){return callback(new Error('密码不可为空'))}else{// 正则表达式校验密码if (value.length > 8 && value.length < 30 && value.replace(/[a-zA-Z0-9]/g, '').length > 0) {if (_userAccount && (value.includes(_userAccount) || value == _userAccount)) {return callback(new Error('密码中不允许出现用户名或与用户名相同'))}callback()}else {return callback(new Error('密码长度8~30位,且必须包含字母、数字、特殊字符'));}}
}
漏洞描述:用户登录帐号与密码、用户姓名、身份证、电话等关键信息不可明文传输
整改方式:采用加密处理,切记不可只采用单层md5加密,虽然md5不可解密,但是现在采用字典暴力破解的可能性也越来越大
(1)可以在加密时加入salt,减小被碰撞破解的可能;或者采用对称或者非对称加密;
(2)对称加密加密与解密使用的是同样的密钥,速度快,但由于需要将密钥在网络传输,所以安全性不高。
(3) 非对称加密使用了一对密钥,公钥与私钥,所以安全性高,但加密与解密速度慢。
(4) 适当的解决的办法是将对称加密的密钥使用非对称加密的公钥进行加密,然后发送出去,接收方使用私钥进行解密得到对称加密的密钥,然后双方可以使用对称加密来进行沟通。
现象描述: 在信息安全测试的项中,竟然要求禁止除GET和POST之外的HTTP方法,开始不是很理解,查询之后说法如下:
一、HTTP请求方法有哪些
HTTP1.0定义了三种请求方法: GET、POST、HEAD
HTTP1.1新增了五种请求方法:OPTIONS、PUT、DELETE、TRACE 、CONNECT
二、举例说明不安全的HTTP方法
GET、POST是最为常见方法,而且大部分主流网站只支持这两种方法,因为它们已能满足功能需求。其中,GET方法主要用来获取服务器上的资源,而POST方法是用来向服务器特定URL的资源提交数据。而其它方法出于安全考虑被禁用,所以在实际应用中,九成以上的服务器都不会响应其它方法,并抛出404或405错误提示。以下列举几个HTTP方法的不安全性:
1、OPTIONS方法,将会造成服务器信息暴露,如中间件版本、支持的HTTP方法等。
2、PUT方法,由于PUT方法自身不带验证机制,利用PUT方法即可快捷简单地入侵服务器,上传Webshell或其他恶意文件,从而获取敏感数据或服务器权限。
3、DELETE方法,利用DELETE方法可以删除服务器上特定的资源文件,造成恶意攻击。
建议把除了GET、POST的HTTP方法禁止,有两方面原因:
1、除GET、POST之外的其它HTTP方法,其刚性应用场景较少,且禁止它们的方法简单,即实施成本低;
2、一旦让低权限用户可以访问这些方法,他们就能够以此向服务器实施有效攻击,即威胁影响大。
整改建议:配置仅支持GET,POST方法即可
关于涉及到上传文件的接口一定要严格校验,包括格式,文件内容,文件名进行严格校验
特点:用户必须登录
原理:利用网站漏洞去自动执行一些接口
举例:
防御措施
1.Token 验证2.请求头中Referer验证 页面来源判断3.隐藏令牌 在http头或者请求中添加隐藏的口令其中一个是对 referer 的校验通用做法,也就是只允许我们制定的域名发送的请求,其他站点则认为是非法请求,比如下面我们的中间件,只要存在 referer,并且 referer 不在我们的域名白名单下,那么则直接返回 403 拒绝访问。
const baseFun = require('../lib/baseFun');
const whiteList = ['127.0.0.1:3000'
];
module.exports = function () {return async function ( ctx, next ) { if(ctx.request.headers.referer && !whiteList.includes(ctx.request.headers.referer)){baseFun.setResInfo(ctx, false, 'access have been forbidden', null, 403);return; } return await next();}
}
另外一个是后台服务在写操作时,使用 token 校验方式,这里我们应用的是 JWT,也就是在用户打开页面时,将 token 写入页面的隐藏元素(或者隐藏域)中,当请求接口时从页面元素中获取 token,再传递到接口参数中,这样第三方站点因为没有打开页面是无法获取到这个 token 的。
表单等隐藏域案例如下:
<``input` `name``=``"authenticity_token"` `type``=``"hidden"` `value``=``"lr/g+5G/gLUzIhYpJwdtULW5afvcf8soZObMznkvxT0="` `/>
CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。
可以在 HTTP 请求头中以参数的形式加入 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于 session 之中,然后在每次请求时把 token 从 session 中拿出,与请求中的 token 进行比对,
漏洞描述:身份凭证信息token不可直接在url中显示,最好也不要存放在cookie中,以防止直接复制进行伪操作
整改建议:避免在url中直接传输用户凭证信息等重要敏感信息数据
Tips: 如果数据存储在Cookies中,那么只要是服务是同域的,(也可以通过nginx实现多个服务的同域),cookies也是可以共享的
漏洞描述:登录后,会话长时间无操作不会自动注销
但是此处如果正常操作下token过期,如何重新获取token保证用户的正常使用;有的策略会在用户持续访问后台接口的时候更新token的过期时间,但是单纯的修改token的过期时间会被攻击利用,从而一致不失效,这样是不安全的。
失效之后的处理策略一般会做两种处理:
一种是直接跳转到登录页面,重新登录;
第二种是:如果返回 token失效的信息,自动去刷新token,然后继续完成未完成的请求操作。
第一种不需要解释,针对第二种,此时我们要考虑一个问题,通常一个页面中不只是发送一个异步请求,可能会同时发送多个异步请求,并且多个请求都需要验证token,如果一个过期了,需要刷新token,那其他请求在那一瞬间之后都会过期,除了第一次判断token失效后,进行刷新token的操作,其余的刷新token都是多余的。
伪代码实现如下:
const axios = require("axios"); // 封装请求 function request(url, options) {const token = localStorage.getItem('token');const defaultOptions = {headers: {Authorization: `Bearer ${token}`,},withCredentials: true,url: url,baseURL: BASE_URL,};const newOptions = { ...options, ...defaultOptions };return axios.request(newOptions).then(checkStatus).catch(error => console.log(error)); } // 默认刷新标识位 let isRefreshing = true; function checkStatus(response) {if (response && response.code === 401) {// 刷新token的函数,这需要添加一个开关,防止重复请求if(isRefreshing){refreshTokenRequst()}isRefreshing = false;// 将当前的请求保存在观察者数组中const retryOriginalRequest = new Promise((resolve) => {addSubscriber(()=> {resolve(request(url, options))})});return retryOriginalRequest;}else{return response;} } function refreshTokenRequst(){let data;const refreshToken = localStorage.getItem('refreshToken');data={authorization: 'YXBwYXBpczpaSWxhQUVJdsferTeweERmR1praHk=',refreshToken,}axios.request({baseURL: BASE_URL,url:'/classList',method: 'POST',data,}).then((response)=>{// 刷新完成后,将刷新token和refreshToken存储到本地localStorage.setItem('refreshToken',response.data.refreshToken);localStorage.setItem('token',response.data.token);// 并且将所有存储到观察者数组中的请求重新执行。onAccessTokenFetched();// 刷新标识位撕掉isRefreshing = true;}); } // 观察者 let subscribers = []; function onAccessTokenFetched() {subscribers.forEach((callback)=>{callback();})subscribers = []; } function addSubscriber(callback) {subscribers.push(callback) }
可以看到刷新标识位相当于变量isRefreshing,观察者相当于数组subscribers。以上便是token失效时的处理策略
漏洞描述:如果不是单点登录,则同一账号可多处进行登录,即浏览器A登录系统,再使用浏览器B使用同一账号登录系统,A浏览器会话未被关闭。
整改建议:如果系统具有单点登录需求,则需要在下一个地点登录通过后,终止上一个地点的会话
特点:用户无需登录
攻击原理:向页面注入js脚本运行
XSS举例:
· 新浪博客写一篇文章,同时偷偷插入一段
· 攻击代码中,获取 cookie,发送自己的服务器
· 发布博客,有人查看博客内容
· 会把查看者的cookie发送到攻击者的服务器
防御措施
**```
npm install vue-xss --save
main.js
import VueXss from “vue-xss”;
Vue.use(VueXss);
xxx.vue中
对于服务端接口而言,尽量不要直接吐出数据,而是统一经过处理层进行转化。
class Xss extends Controller {index() {const params = querystring.parse(this.ctx.request.querystring);let name = decodeURI(params['name']);return this.ctx.response.body = name;//return this.resApi(true, 'good', a);}
}
module.exports = Xss;
没有对 name 进行任何处理,就直接返回给接口,并且没有经过我们的 resApi 服务,而是使用 body 直接设置返回*。这样就会导致,*当 name 为一个 HTML 或者 JS 都会被浏览器执行*。当我们启动服务后,访问以下地址,都会出现一些异常问题。
*http://127.0.0.1:3000/v1/xss/index?name=%3Cscript%3Ealert(%27nodejs%27)%3C/script%3E http://127.0.0.1:3000/v1/xss/index?name=%3Chtml%3E%3Ch1%3E%E6%88%91%E6%83%B3%E6%89%93%E5%8D%B0%E4%BB%80%E4%B9%88%EF%BC%8C%E5%B0%B1%E6%89%93%E5%8D%B0%E4%BB%80%E4%B9%88%EF%BC%8C%E4%BD%A0%E7%BD%91%E7%AB%99%E8%A2%AB%E6%94%BB%E5%87%BB%E4%BA%86%3C/h1%3E%3C/html%3E
这里最简单的防御方式就是使用我们统一的 resApi 处理响应数据,因为在 resApi 中固定了返回结果,进行了 JSON.stringify 处理,所有的返回都会封装为一个 json 字符串,因此不会存在 XSS 的问题。
**
重放攻击(Replay Attacks)是指攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的正确性。 它是一种攻击类型,这种攻击会不断恶意或欺诈性地重复一个有效的数据传输,重放攻击可以由发起者,也可以由拦截并重发该数据的敌方进行。攻击者利用网络监听或者其他方式盗取认证凭据,之后再把它重新发给认证服务器。
加密可以有效防止会话劫持,但是却防止不了重放攻击。
1.时间戳
A接收一个消息当且仅当其包含一个对A而言足够接近当前时刻的时间戳,而被重放的时戳将相对远离当前时刻 ;但是这必须要求通信各方的计算机时钟保持同步;设置大小适当的时间窗(间隔),越大越能包容网络传输延时,越小越能防重放攻击。
但是这样有个缺点就是:在连接情形下双方时钟若偶然出现不同步,则正确的信息可能会被误判为重放信息而丢弃,而错误的重放信息可能会当作最新信息而接收
2.序号
通信双方通过消息中的序列号来判断消息的新鲜性
要求通信双方必须事先协商一个初始序列号,并协商递增方法
3.提问与应答
“现时”──与当前事件有关的一次性随机数N(互不重复即可)
基本做法──期望从B获得消息的A 事先发给B一个现时N,并要求B应答的消息中包含N或f(N),f是A、B预先约定的简单函数
原理──A通过B回复的N或f(N)与自己发出是否一致来判定本次消息是不是重放的
常规流程
1.前端web页面用户输入账号、密码,点击登录。
2.请求提交之前,web端首先通过客户端脚本如javascript对密码原文进行md5加密。
3.提交账号、md5之后的密码
4.请求提交至后端,验证账号与密码是否与数据库中的一致,一致则认为登录成功,反之失败。
有什么问题呢?
上述流程看似安全,认为传输过程中的密码是md5之后的,即使被监听截取到,由于md5的不可逆性,密码明文也不会泄露。
其实不然!监听者无需解密出密码明文即可登录!监听者只需将监听到的url(如:http://****/login.do?method=login&password=md5之后的密码&userid=登录账号)重放一下,即可冒充你的身份登录系统。
稍微安全点的方式
1.进入登陆页面时,生成一个随机码(称之为盐值),在客户端页面和session中各保存一份。
2.客户端提交登录请求时,将md5之后的密码与该随机码拼接后,再次执行md5,然后提交(提交的密码=md5(md5(密码明文)+随机码))。
3.后端接收到登录请求后,将从数据库中查询出的密码与session中的随机码拼接后,md5运算,然后与前端传递的结果进行比较。
为何要这样?
该登录方式,即使登录请求被监听到,回放登录URL,由于随机码不匹配(监听者的session中的随机码与被监听者的session中的随机码相同概率可忽略),无法登录成功。 该登录方式,由于传输的密码是原密码md5之后与随机码再次md5之后的结果,即使监听者采用暴力破解的方式,也很难解密出密码明文。
简单密码的md5结果很容易通过暴力破解的方式给解密出来,何况md5出现了这么多年,可能已经有不少字典了!同时为了方便用户登录的方便性,我们的系统一般不可能要求用户设置很长、很复杂的密码!怎么办?加固定盐值。
1.系统设置一个固定的盐值,该盐值最好足够复杂,如:1qaz2wsx3edc4rfv!@#$%^&qqtrtRTWDFHAJBFHAGFUAHKJFHAJHFJHAJWRFA
2.用户注册、修改密码时,将用户的原始密码与我们的固定盐值拼接,然后做md5运算。
3.传递至后端,保存进数据库(数据库中保存的密码是用户的原始密码拼接固定盐值后,md5运算后的结果)。
4.登录时,将用户的原始密码与我们的固定盐值进行拼接,然后做md5运算,运算后的结果再拼接上我们的随机码,再次md5运算,然后提交。
5.后端接收到登录请求后,将从数据库中查询出的密码与session中的随机码拼接后,md5运算,然后与前端传递的结果进行比较。
再进一步
1.加登录验证码,可预防人为地暴力登录破解
2.账户锁定,如果用户密码输入错误次数达到一定量后(如6次),则可以锁定该账号
前端:
const crypto = require('crypto');
const md5 = require('md5');
// 加密秘钥
var AES_conf = {key: "ESS-2019$05#sb%_", //密钥iv: '1012132405963708' //偏移向量
}
export default{/** * AES_128_CBC 加密 * 128位 * return base64 */encryption:(data) => {let key = AES_conf.key;let iv = AES_conf.iv;var cipherChunks = [];var cipher = crypto.createCipheriv('aes-128-cbc', key, iv);cipher.setAutoPadding(true);cipherChunks.push(cipher.update(data, 'utf8', 'base64'));cipherChunks.push(cipher.final('base64'));return cipherChunks.join('');},/** * 解密 * return utf8 */decryption:(data) => {let key = AES_conf.key;let iv = AES_conf.iv; var cipherChunks = [];var decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);decipher.setAutoPadding(true);try {cipherChunks.push(decipher.update(data, 'base64', 'utf8'));cipherChunks.push(decipher.final('utf8'));return{code: 200, data: cipherChunks.join('')};} catch (error) {return{code: 400, data: '密文错误,解密失败'};}},// 数据完整性MD5字段生成generateIntegrityVal(data){return md5(JSON.stringify(data))},// 数据完整性校验integrityChecke(data, integrityValue){return (md5(JSON.stringify(data)) === integrityValue)},//生成一定范围内的随机数random(start, end) {return parseInt(start + Math.random() * (end - start))},/** * @description: 生成防重放数字签名 */generateSign() {let timestamp = new Date().getTime(),nonce = parseInt(this.random(0,100000000)),//建议加上用户uid,组合生成signsign = md5(timestamp + nonce);let val = {timestamp,nonce,sign}return JSON.stringify(val)}
}
axios拦截器
// http request 拦截器
axios.interceptors.request.use(request => {// 防重放签名request.headers['sign'] = tools.generateSign()return request;},err => {}
);
node expressjs服务端代码
// CSRF:验证referer头 app.use(function (req, res, next) {let referer = req.get('Referrer') || req.headers.referer || req.headers.hostlogger.info('referer:', referer)if (<img src="http://')) {referer = referer.substring(7)}if (referer.endsWith('/')) {referer = referer.substring(0, referer.length - 1)}console.log('endReferer',referer)if (config._systemConf.safeReferers.includes(referer)) {next();} else {next({status: 403,message: "Your request was Forbidden!"})}" style="margin: auto" /> }); //防重放 map数组 var signMap = tools.signMap; //防重放函数 app.use((req, res, next) => {// 静态文件不做防重放if(req.path == '/' || tools.interceptFilter(req, ['.jpg', '.png', '.ico','.icon', '.html', '.jpeg', 'dist', '.js', '.css'])){next()}else{if(!req.headers.sign){next({status: 403,message: "Your request was Forbidden!!"})}else{//获取sign对象let signObj = JSON.parse(req.headers.sign);//根据传入时间戳以及随机数生成signlet sign = md5(signObj.timestamp + signObj.nonce);logger.info('重放时间:',(new Date().getTime() - signObj.timestamp), config._systemConf.ReplayTime)//检验是否与前端生成的sign一致,判断请求时间间隔是否小于60s,判断此次sign不存在于signMap数组中if (signObj && sign == signObj.sign && (new Date().getTime() - signObj.timestamp) < config._systemConf.ReplayTime && signMap.every(item => item.sign != sign)) {signMap.push(signObj);next()} else {next({status: 403,message: "Your request was Forbidden!!!"})}}} }) tools.js /** * Created by Administrator on 2017/6/21. */ "use strict" const crypto = require('crypto'); const md5 = require('md5'); // 加密秘钥要和前端一致 var AES_conf = {key: "ESS-2019$05#sb%_", //密钥iv: '1012132405963708' //偏移向量 } module.exports = {/** * AES_128_CBC 加密 * 128位 * return base64 */encryption: (data) => {let key = AES_conf.key;let iv = AES_conf.iv;var cipherChunks = [];var cipher = crypto.createCipheriv('aes-128-cbc', key, iv);cipher.setAutoPadding(true);cipherChunks.push(cipher.update(data, 'utf8', 'base64'));cipherChunks.push(cipher.final('base64'));return cipherChunks.join('');// return cipherChunks.join('').replace(new RegExp('=','g'),'')},/** * 解密 * return utf8 */decryption: (data) => {// let add = data.length%3;// for(let i = 0 ;i<add ;i++){// data+='='// }let key = AES_conf.key;let iv = AES_conf.iv;var cipherChunks = [];var decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);decipher.setAutoPadding(true);try {cipherChunks.push(decipher.update(data, 'base64', 'utf8'));cipherChunks.push(decipher.final('utf8'));return {code: 200,data: cipherChunks.join('')};} catch (error) {return {code: 400,data: '密文错误,解密失败'};}},// 数据完整性校验integrityChecke(data, integrityValue) {return (md5(JSON.stringify(data)) === integrityValue)},/** * @description: 生成一定范围内的随机数 * @param {start} * @param {end} * @return: */random(start, end) {return parseInt(start + Math.random() * (end - start))},/** * @description: 防重放请求仓库(对象数组) */signMap: [],// 生成防重放数字签名generateSign() {let timestamp = new Date().getTime(),nonce = parseInt(this.random(0,100000000)),//建议加上用户uid,组合生成signsign = md5(timestamp + nonce);return {timestamp,nonce,sign}},/** * * 拦截筛选函数 */interceptFilter(req, filterArr){let ifFilter = false; // 初始全部拦截for(let filter of filterArr){if(req.path.includes(filter)){ifFilter = true// 包含过滤器中的字段就不拦截}}return ifFilter} };
用户登录和修改密码等关键接口需要做
可以在请求头中增加完整性integrity字段,或者像下面这样单独加一个参数用来存储
tools.js // 数据完整性MD5字段生成generateIntegrityVal(data){return md5(JSON.stringify(data))},
---------------------------------------------------------
updatePassword: function (userID, oldPwd, newPwd, callback) { let _params = { id: userID, oldPwd: tools.encryption(oldPwd), // 敏感数据加密 newPwd: tools.encryption(newPwd) } let integrityValue = tools.generateIntegrityVal(_params)// 数据完整性MD5字段生成,后端做校验 axios.post('/uums/users/updatePassword', { userId: _params.id, oldPwd: _params.oldPwd, newPwd: _params.newPwd, integrity: integrityValue }).then((res) => { callback(res.data); });
}
注入方式举例:
原sql,${id} 为传入参数
select * from tbl_topic_info wheretopic_id = ${id}
正常情况,传入的是正常的字符串 1
select * from tbl_topic_info wheretopic_id = '1'
注入情况一:传来的参数是 1';SELECT pg_sleep(5)--
select * from tbl_topic_info wheretopic_id = '1';SELECT pg_sleep(5)--'
此时应为有分号,导致查询变成了两条sql,sleep 也执行了
注入情况二:传来的参数是 1' or 1 = 1 --;
select * from tbl_topic_info wheretopic_id = '1' or 1 = 1 --;'
这样就会获取到所有数据,导致数据泄露
但凡有SQL注入漏洞的程序,都是因为程序要接受来自客户端用户输入的变量或URL传递的参数,并且这个变量或参数是组成SQL语句的一部分,对于用户输入的内容或传递的参数,我们应该要时刻保持警惕,这是安全领域里的「外部数据不可信任」的原则,纵观Web安全领域的各种攻击方式,大多数都是因为开发者违反了这个原则而导致的,所以自然能想到的,就是从变量的检测、过滤、验证下手,确保变量是开发者所预想的。
永远不要信任来自用户端的变量输入,有固定格式的变量一定要严格检查对应的格式,没有固定格式的变量需要对引号等特殊字符进行必要的过滤转义。
1、检查变量数据类型和格式
2、过滤特殊符号
3、绑定变量,使用预编译语句
在 Node.js 中有最常见的方法是使用占位符(即预编译语句)的方式,也就是使用下面的方式替代拼装 SQL 语法的方法
connection.query('SELECT * FROM student WHERE name = ?', [name], function(err, results) {})
相信大家都还对2011年爆出的CSDN拖库事件记忆犹新,这件事情导致CSDN处在风口浪尖被大家痛骂的原因就在于他们竟然明文存储用户的密码,这引发了科技界对用户信息安全尤其是密码安全的强烈关注,我们在防范SQL注入的发生的同时,也应该未雨绸缪,说不定下一个被拖库的就是你,谁知道呢。
在Web开发中,传统的加解密大致可以分为三种:
1、对称加密:
即加密方和解密方都使用相同的加密算法和密钥,这种方案的密钥的保存非常关键,因为算法是公开的,而密钥是保密的,一旦密匙泄露,黑客仍然可以轻易解密。常见的对称加密算法有:AES、DES等。
2、非对称加密:
即使用不同的密钥来进行加解密,密钥被分为公钥和私钥,用私钥加密的数据必须使用公钥来解密,同样用公钥加密的数据必须用对应的私钥来解密,常见的非对称加密算法有:RSA等。
3、不可逆加密:
利用哈希算法使数据加密之后无法解密回原数据,这样的哈希算法常用的有:md5、SHA-1等。
但是md5已经被通常的做法是为每个用户确定不同的密码加盐(salt)后,再混合用户的真实密码进行md5加密
这种网络攻击的主要原理就是通过模拟无效的海量用户请求,来导致后台服务的崩溃现象。一般这种在后台服务中不需要考虑,可以直接在网关层进行处理,如果真的没有网关层的话,可以考虑使用 NPM 中的一个ddos 库(www.npmjs.com/package/ddo… )。
在任何情况下,都应杜绝使用该函数,因为该函数存在非常不可控的因素,这点和 SQL 注入相似,相当于 JS 代码注入,比如下面这段代码:
const querystring = require('querystring');
const Controller = require('../../core/controller');
class Eval extends Controller {index() {const params = querystring.parse(this.ctx.request.querystring);// 获取参数 rlet r = decodeURI(params['r']);// 根据参数 r 动态调用 this._p() 获取执行结果let ret = eval(`this._q() + ${r}`);return this.resApi(true, 'good', ret);}_q () {return 1;}_p () {return 2;}
}
module.exports = Eval;
代码比较简单,假设我们希望用 eval 来动态调用内部的一些函数,因此我们使用了 r 这个参数,正常情况下是可以调用,但是如果我们调用下面的地址:
http://127.0.0.1:3000/v1/eval/index?r=this._p();console.log('d');const fs=require('fs');fs.readFileSync(__filename, 'utf8')
访问后,你会发现很恐怖的一幕,源代码直接被返回了,如图 1 所示。
图 1 eval 泄漏源代码例子
因此无论任何情况下,代码中都不允许 eval 的使用,因为不可控因素太大。
在我们之前设计路由的时候,有讲解过这个路径的问题,就像下面这段代码:
// 去除非常规请求路径,将-转化为大写
pathname = pathname.replace('..', '').replace(/\-(\w)/g, (all,letter)=>letter.toUpperCase());
第一个 replace 的两个点是非常重要的,这样我们才能控制 require 的文件仅仅只在 controller 文件夹下。
接下来我们看一个没有控制好目录路径导致的问题,比如下面这段代码:
class Fs extends Controller {index() {const params = querystring.parse(this.ctx.request.querystring);// 根据产品名称获取产品的配置信息let product = decodeURI(params['product']);try {let productInfo = fs.readFileSync(`${__dirname}/../../config/products/${product}.json`, 'utf8');return this.resApi(true, 'good', productInfo);} catch(err){return this.resApi(false, 'can not find the product');}}
}
正常访问以下两个链接都可以拿到我们具体需要的正常逻辑。
http://127.0.0.1:3000/v1/fs/index?product=c
http://127.0.0.1:3000/v1/fs/index?product=d
正常的返回结果如下:
{
code: 200.
msg: '',
data: "{
name: 'john',
age: 23
}"
}
但是如果我们访问了以下地址,就直接导致了配置文件泄漏,从而引发了数据库账号和密码被泄漏的安全问题。
http://127.0.0.1:3000/v1/fs/index?product=../b
访问以后,你就可以通过上面代码访问到我们项目中的所有配置文件了,而配置文件中包含了非常多敏感信息:
{
code: 200.
msg: '',
data: "{
database: 'xxx',
user: 'lilei',
pwd:'xxx12334dd&'
}"
}
解决方案的话,就是将访问的配置文件,控制在当前配置文件目录下,因此你需要将这种 … 路径进行替换,比如使用下面代码修复后,就解决了该问题。你再次启动服务,访问上面路径后,将会提示访问路径异常的信息。
// 去掉上层目录访问
product = product.replace('..', '')
但是这里要注意,这样还是可以访问同目录下的文件的,因此最好的方式是,将配置文件归类,并且做好校验,非范围内的配置文件不允许读取。
其次在写文件时,更加要注意风险问题,一般情况下,分开写目录和源代码目录,例如可以将上传的文件或者日志文件放到另外一个单独目录,并控制权限即可。以防代码写漏洞,导致本地文件被篡改,或者写入一些脚本文件从而控制服务器。
在大部分情况下 Node.js 的进程是无须太多权限的,只需要一些固定目录的读写权限,因此我们只需要赋予 Node.js 服务最低的用户权限,一定不要设置为 root 权限。比如上面我们的 eval 函数导致的问题,如果你是使用 root 权限,那么就可以通过 Node.js fs 获取主机的登录密码,从而直接控制这台机器。而在大部分公司,主机和主机都是内网互通,如果单台内网机器被攻克后,就相当于整个公司的内网系统沦陷了。
为了解决这个问题,我们可以新建一个独立的用户,然后创建 Node.js 所需要读写的日志以及其他目录权限赋予读写权限,如下所示:
adduser username
chown -R /path
第一步创建用户,第二步为用户归属权限,一般情况下只需要归属当前源代码路径和需要写日志的目录。
这部分内容来源于拉钩教育课程《Nodejs 应用开发实战》: kaiwu.lagou.com/course/cour…
在客户端产生请求时,对接口url进行RSA加密处理。
假设我们本来需要访问 api.example.com/articles 这样的一个接口,接口返回json数据。在客户端访问之前,我们先对这个url进行这样的处理:
1.加客户端时间戳:api.example.com/1322470148/…
2.对url的path段进行rsa加密,然后base64:api.example.com/TBhIskCgCN+…
我们真实访问的地址就变成了这样一个长长的 url 结构,我们通过rsa算法的padding参数和时间戳,就可以让这个后面长长的bas64串在每次访问的时候都发生变化,同时,我们可以在服务器端把一个小时之内的请求过的串都记下来,并不让再次访问,这样就防止了爬虫的重放请求尝试。
在服务器端,我们就需要在做响应之前,把url还原回来,然后返回响应数据即可。
**
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。