Dawn's Blogs

分享技术 记录成长

0%

Go Web编程学习笔记 (6) 安全和加密

CSRF

CSRF 原理

CSRF(Cross-Site Request Forgery)跨站请求伪造,可以伪装为受害者的身份,向服务器发送各种请求。原理如下:

  • 受害者登录受信任网站 A,并在本地生成 Cookie
  • 在不退出 A 的情况下,访问危险网站 B。此时访问 B 时会发送请求给受信任网站 A 并且会附上 Cookie 信息。

img

CSRF 防御

在服务器端防御 CSRF,主要有两个方面:

  • 正确使用 GET、POST 请求。
  • 在非 GET 请求中增验证。

正确使用 GET POST 请求

使用 REST 方式可以限制请求的类型:

  • GET 常用在查看,列举,展示等不需要改变资源属性的时候。

  • POST 常用在改变一个资源的属性或者状态。

增加验证

在非 GET 请求中增加验证,可以有三个思路:

  • 为每个用户生成一个唯一的 token,所有表单都包含同一个伪随机值。这种方法最简单,因为攻击者(理论上)不能获取到第三方的 Cookie,所以表单中的数据也就构造失败,但是 XSS 可以窃取到第三方 Cookie,所以这个方案在没有 XSS 时是安全的。

  • 为每一个请求使用验证码,用户体验很差。

  • 每个用户生成的 token 随时更新,实现如下:

    • 生成随机 token:
    1
    2
    3
    4
    5
    6
    7
    h := md5.New()
    io.WriteString(h, strconv.FormatInt(crutime, 10))
    io.WriteString(h, "salt")
    token := fmt.Sprintf("%x", h.Sum(nil))

    t, _ := template.ParseFiles("xxx.gtpl")
    t.Execute(w, token)
    • 表单中的 token:
    1
    <input type="hidden" name="token" value="{{.}}">
    • 验证 token:
    1
    2
    3
    4
    5
    6
    7
    r.ParseForm()
    token := r.Form.Get("token")
    if token != "" {
    // 验证 token 的合法性
    } else {
    // 不存在 token 报错
    }

XSS

XSS 原理

XSS(Cross-Site Scripting)跨站脚本攻击,原理是一段恶意的 JavaScript 代码在用户客户端上被执行,导致信息泄露( Cookie 泄露)。XSS 主要用于攻击用户端的。

主要分为两类:

  • 存储型 XSS:恶意 XSS 代码被服务器存储到了服务器中,应用程序从数据库中查询出来并在客户端中显示,造成 XSS 攻击。
  • 反射型 XSS:将恶意 XSS 代码加入到 URL 的请求参数中,请求参数在页面上直接输出。

XSS 防御

XSS防御可以有如下方式:

  • 过滤特殊字符:text/template 包下面的 HTMLEscapeString、JSEscapeString 等函数可以对敏感字符进行转义。
  • 输入内容长度控制:对于不受信任的输入,都应该限定一个合理的长度,这样可以增加攻击难度。
  • HTTP-Only:禁止从客户端脚本中读取 Cookie 信息,使得攻击者无法窃取 Cookie。
  • 等一系列防御手段。。。

SQL 注入

SQL 注入原理

SQL 注入的原理就是因为用户输入的数据被当作 SQL 语句执行。

SQL 注入防御

SQL 注入可以有如下防御方式:

  • 限制 Web 应用数据库的操作权限,给予用户最低的操作权限。

  • 检查输入的数据,对进入数据库的字符进行转义、过滤。html/template 包的 HTMLEscapeString 函数可以对字符串进行转义处理。

  • 所有的查询语句建议使用数据库提供的参数化查询接口,避免直接拼接 SQL 语句。

存储密码

普通方案:哈希

目前利用最多的方案就是对明文密码进行哈希之后,进行存储。常用的单向哈希算法包括 SHA-256, SHA-1, MD5 等。

缺点:考虑到多数人所使用的密码为常见的组合,攻击者可以将所有密码的常见组合进行单向哈希,得到一个摘要组合(彩虹表),然后与数据库中的摘要进行比对即可获得对应的密码。

进阶方案:哈希+盐

可以采用加盐的方式来存储密码,常用的方式:

  • 对用户的明文密码进行一次哈希运算
  • 将得到的摘要加上随机串(盐),这个随机串中可以包括某些固定的串,也可以包括用户名(用来保证每个用户加密使用的密钥都不一样)。
  • 再进行一次哈希运算后,放入数据库中存储起来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 假设用户名 abc,密码 123456
h := md5.New()
io.WriteString(h, "需要加密的密码")

// pwmd5 等于 e10adc3949ba59abbe56e057f20f883e
pwmd5 :=fmt.Sprintf("%x", h.Sum(nil))

// 指定两个 salt: salt1 = @#$% salt2 = ^&*()
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

故意增加密码计算所需耗费的资源和时间,使得任何人都不可获得足够的资源建立所需的 rainbow table

Scrypt 算法使得并行计算多个摘要异常困难,因此利用rainbow table(彩虹表)进行暴力攻击的难度增加。

在 Go 的 golang.org/x/crypto/scrypt 包中支持 scrypt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"encoding/base64"
"fmt"
"log"

"golang.org/x/crypto/scrypt"
)

func main() {
// DO NOT use this salt value; generate your own random salt. 8 bytes is
// a good length.
salt := []byte{0xc8, 0x28, 0xf2, 0x58, 0xa7, 0x6a, 0xad, 0x7b}

dk, err := scrypt.Key([]byte("some password"), salt, 1<<15, 8, 1, 32)
if err != nil {
log.Fatal(err)
}
fmt.Println(base64.StdEncoding.EncodeToString(dk))
}

加密和解密数据

Go 语言中 crypto 及其子包提供了多种加密算法。