在进行 nodejs 开发后台服务过程中,我遇到了如 xss 攻击,sql 注入,密码加密等安全问题,现整理成此文。
sql
注入
自从互联网可以让用户发布信息和提交信息开始就有了 sql 注入。
它的攻击方式很简单,在输入框、文本域中输入带有 sql 语句的内容,并传给后台,后台将前端传过来的 sql 语句不加处理,最终拼接成一段攻击代码。
所以,必须加以预防,不然有可能出现数据泄露,甚至被删库。
下面以登录为例来演示如何进行 sql 攻击。
此时,有一个 HTML
页面用于登录。
但如果此时,用户名输入的是 zhangsan' --
,注意 --
前后都有空格,那密码就可以随便输入了。
最后拼接出来的 SQL
语句是 select username, realname from users where username='zhangsan' -- ' and password='111';
在 MySQL
里, --
代表注释的意思。所以上面的语句就变成查询 username 为 zhangsan 的那条数据,自然就绕过了密码,不必验证密码了。
这还不是最严重的,但如果别人要删掉你的表,那后果就非常严重了。
比如,在用户名输入框内输入:zhangsan'; delete from users; --
。直接就把 users
表给删掉了。
防止方法的方法也非常简单:将前端传过来的字符串进行转码。下载的 MySQL
依赖包里就提供了这个方法:escape
。
const mysql = require('mysql')
const escape = mysql.escape
const login = (username, password) => {
username = escape(username)
password = escape(password)
const sql = `
select username, realname from users where username=${username} and password=${password};
`
}
此时如果用户名输入 zhangsan' --
,在后端控制台会打印出如下内容:
select username, realname from users where username='zhangsan\' -- ' and password='1111';
可以看到 zhangsan'
后面的单引号被转义了,变成了zhangsan\'
。
除了登录,搜索框等也要用escape
执行一遍,因为搜索内容的时候可以输入关键字,在输入关键字的时候也可以添加' --
来实现 sql 注入。
所以,所有可以用来输入内容的接口都要防止 sql 注入。
xss 攻击
xss 攻击这应该是前端同学最熟悉的攻击方式了,但是,后端更应该需要了解。它的攻击方式很简单,在页面展示内容中掺杂 js 代码,以获取用户信息。
因此,它的预防措施就是转换生成 js 代码的字符串。比如:
什么意思呢?
因为在输入内容的时候很可能输入 js 代码块,如<script>alert(1)</script>
,那如果把<>
这种符号转义下,那它就不是 js 代码块了。
比如,在创建博客的页面中,我在文本框中输入<script>alert(document.cookie)</script>
,这段代码如果不经过处理,就会直接存储到数据库中,当用户请求博客时这一段代码就会在页面中执行,暴露用户信息。
此时,只需要安装第三方库来转义就好了。
npm i xss --save
const title = xss(blogData.title)
const content = xss(blogData.content)
数据库密码加密
如果你的数据库被攻破了,同时用户的密码全是明文的,那么他就可以拿着密码去登录其他系统,因为大多数人使用的密码就那么几个。
预防措施就是对密码加密,即使黑客拿到密码了也不知道明文。
最常用最简单的加密方式就是利用md5
算法进行加密。
md5 是一种数字摘要算法,它可以将任意长度的字符串转换成一个128位的散列值。
它最常用于加密用户密码,它可以将用户输入的明文密码转换成一个128位的散列值,然后存储在数据库中,而不必将用户的密码明文存储,从而保护用户的密码安全。此外,md5还可以用于文件完整性检查,可以检查文件是否被篡改。
虽然,从技术的角度来说,md5 是真的安全,因为它是不可逆的,没办法解密。但是,可以通过撞库的方式进行暴力破解,黑客会将一些常用的密码汇总成一个表,即彩虹表,然后一个一个去试,直到破解。
因为用户习惯用容易记住的密码,比如手机号、生日等,不法分子容易获取这类密码,所以需要加盐。即,md5 hash + salt
(盐)保存。
盐(Salt
),在密码学中,是指在hash之前将明文内容(例如:密码)的任意固定位置插入特定的字符串。如下:
MD5('123' + '1ck12b13k1jmjxrg1h0129h2lj') = '6c22ef52be70e11b6f3bcf0f672c96ce'
username: 'zhangsan'
password: 6c22ef52be70e11b6f3bcf0f672c96ce
salt: 1ck12b13k1jmjxrg1h0129h2lj
这样密码被破解的难度会大大降低,同时你也可以把盐(Salt
)通过自己定义的某种方式,分开插入到明文特定的位置,然后进行加密,这样黑客即使拿到盐(Salt
),他也不能把密码破解成明文。
但是,这个盐(Salt
)也需要保存在数据库中,这很麻烦。能不能把盐(Salt
)存放到最终生成的密码中呢?
答案是可以的。这就是今天要介绍的bcrypt
加密。
它是一种加盐的单向Hash,不可逆的加密算法,同一种明文(plaintext),每次加密后的密文都不一样,而且不可反向破解生成明文,破解难度很大。
bcrypt('123') = $2b$10$69SrwAoAUNC5F.gtLEvrNON6VQ5EX89vNqLEqU655Oy9PeT
username: zhangsan
password: $2b$10$69SrwAoAUNC5F.gtLEvrNON6VQ5EX89vNqLEqU655Oy9PeT
对123
加密后,最终生成密码就是$2b$10$69SrwAoAUNC5F.gtLEvrNON6VQ5EX89vNqLEqU655Oy9PeT
,其中69SrwAoAUNC5F
就是随机生成的盐,它每次都会随机生成,即每次重新加密后的值都不一样。
下面来看下如何使用bcrypt
。
首先,安装npm install bcrypt
。
接下来,我们需要一个变量:saltRounds。saltRounds
代表加密所需的时间,saltRounds
越大,加密所需时间越长,加密后的密码也就越安全。但是我们也不想让用户永远等着我们加密密码,所以一般使用默认值10就可以。
const saltRounds = 10;
const password = "123456";
现在,利用Bcrypt
的genSalt和hash方式来生成并且存储我们最终生成的hash。首先,我们先使用bcrypt.genSalt
会接收saltRounds和回调函数,这个回调函数会返回salt
结果和error对象:
bcrypt.genSalt(saltRounds, function(err, salt) {
// 返回salt值或者进行错误处理
});
在得到salt
值后,在回调的内部再调用Bcrypt的hash方式来生成hash。hash接受三个参数,分别是密码,salt,和回调函数。 通常,我们会在这个回调函数里将处理好的hash存储到数据库中,或者进行错误处理。
bcrypt.genSalt(saltRounds, function(err, salt) {
bcrypt.hash(password, salt, function(err, hash) {
// 返回hash的值,并将他存储到数据库中
});
});
但其实,你也可以把以上两个函数合并成一个,结果如下:
bcrypt.hash(password, saltRounds, function(err, hash) {
// 数据库操作和错误处理
});
这个时候,你会发现这时候新注册的用户密码已经变成了一段长长的hash。再当用户登录的时候,我们就不能简单的对比用户输入的密码和数据库的hash了。Bcrypt
为我们提供了一个方法,叫做compare
。
bcrypt.compare()
会接受三个参数,分别是用户输入的密码,数据库中的hash,和回调函数,回调函数会返回一个布尔值,如果密码和hash吻合的话,就返回true,反之false。
const password = "123456";
// hash 从数据库中获取
bcrypt.compare(password, hash, function(err, result) {
if (result) {
console.log("密码正确")
}
else {
console.log("密码错误");
}
});
Bcrypt
的存在为我们的程序多加了一层保障,它利用加盐和散列的算法将我们的密码加密成一段很难破解的长字符串,这样,即使我们的数据库被攻破,hash后的密码也可以给我们提供最后一层保护。
总结
本文是个人在开发过程目前遇到的安全问题,这里主要讲了三个问题:
sql注入
: 把' --
进行转义,使其失去注销的含义xss攻击
: 把特殊字符进行转义,使其失去本来的函数,变成一个纯粹的字符串- 密码加密: 本文介绍了
bcrypt
的加密方式,它是目前安全度很高的一种加密方法。
后续在学习 nodejs 开发后台的过程中肯定会遇到更多的问题,到时候会补充。