# 1.1 输入校验
# 1.1.1【必须】按类型进行数据校验
- 所有外部输入的参数,应使用
validator
进行白名单校验,校验内容包括但不限于数据长度、数据范围、数据类型与格式,校验不通过的应当拒绝
// good
import (
"fmt"
"github.com/go-playground/validator/v10"
)
var validate *validator.Validate
validate = validator.New()
func validateVariable() {
myEmail := "abc@tencent.com"
errs := validate.Var(myEmail, "required,email")
if errs != nil {
fmt.Println(errs)
return
//停止执行
}
// 验证通过,继续执行
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 无法通过白名单校验的应使用
html.EscapeString
、text/template
或bluemonday
对<, >, &, ',"
等字符进行过滤或编码
import(
"text/template"
)
// TestHTMLEscapeString HTML特殊字符转义
func main(inputValue string) string{
escapedResult := template.HTMLEscapeString(inputValue)
return escapedResult
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 1.2 SQL操作
# 1.2.1【必须】SQL语句默认使用预编译并绑定变量
- 使用
database/sql
的prepare、Query或使用GORM等ORM执行SQL操作
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
type Product struct {
gorm.Model
Code string
Price uint
}
...
var product Product
db.First(&product, 1)
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 使用参数化查询,禁止拼接SQL语句,另外对于传入参数用于order by或表名的需要通过校验
// bad
import (
"database/sql"
"fmt"
"net/http"
)
func handler(db *sql.DB, req *http.Request) {
q := fmt.Sprintf("SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='%s' ORDER BY PRICE",
req.URL.Query()["category"])
db.Query(q)
}
// good
func handlerGood(db *sql.DB, req *http.Request) {
//使用?占位符
q := "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='?' ORDER BY PRICE"
db.Query(q, req.URL.Query()["category"])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1.3 网络请求
# 1.3.1【必须】资源请求过滤验证
使用
"net/http"
下的方法http.Get(url)
、http.Post(url, contentType, body)
、http.Head(url )
、http.PostForm(url, data)
、http.Do(req)
时,如变量值外部可控(指从参数中动态获取),应对请求目标进行严格的安全校验。如请求资源域名归属固定的范围,如只允许
a.qq.com
和b.qq.com
,应做白名单限制。如不适用白名单,则推荐的校验逻辑步骤是:第 1 步、只允许HTTP或HTTPS协议
第 2 步、解析目标URL,获取其HOST
第 3 步、解析HOST,获取HOST指向的IP地址转换成Long型
第 4 步、检查IP地址是否为内网IP,网段有:
// 以RFC定义的专有网络为例,如有自定义私有网段亦应加入禁止访问列表。 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.0/8
1
2
3
4
5第 5 步、请求URL
第 6 步、如有跳转,跳转后执行1,否则绑定经校验的ip和域名,对URL发起请求
官方库
encoding/xml
不支持外部实体引用,使用该库可避免xxe漏洞
import (
"encoding/xml"
"fmt"
"os"
)
func main() {
type Person struct {
XMLName xml.Name `xml:"person"`
Id int `xml:"id,attr"`
UserName string `xml:"name>first"`
Comment string `xml:",comment"`
}
v := &Person{Id: 13, UserName: "John"}
v.Comment = " Need more details. "
enc := xml.NewEncoder(os.Stdout)
enc.Indent(" ", " ")
if err := enc.Encode(v); err != nil {
fmt.Printf("error: %v\n", err)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1.4 服务器端渲染
# 1.4.1【必须】模板渲染过滤验证
- 使用
text/template
或者html/template
渲染模板时禁止将外部输入参数引入模板,或仅允许引入白名单内字符。
// bad
func handler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
x := r.Form.Get("name")
var tmpl = `<!DOCTYPE html><html><body>
<form action="/" method="post">
First name:<br>
<input type="text" name="name" value="">
<input type="submit" value="Submit">
</form><p>` + x + ` </p></body></html>`
t := template.New("main")
t, _ = t.Parse(tmpl)
t.Execute(w, "Hello")
}
// good
import (
"fmt"
"github.com/go-playground/validator/v10"
)
var validate *validator.Validate
validate = validator.New()
func validateVariable(val) {
errs := validate.Var(val, "gte=1,lte=100")//限制必须是1-100的正整数
if errs != nil {
fmt.Println(errs)
return False
}
return True
}
func handler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
x := r.Form.Get("name")
if validateVariable(x):
var tmpl = `<!DOCTYPE html><html><body>
<form action="/" method="post">
First name:<br>
<input type="text" name="name" value="">
<input type="submit" value="Submit">
</form><p>` + x + ` </p></body></html>`
t := template.New("main")
t, _ = t.Parse(tmpl)
t.Execute(w, "Hello")
else:
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 1.5 Web跨域
# 1.5.1【必须】跨域资源共享CORS限制请求来源
- CORS请求保护不当可导致敏感信息泄漏,因此应当严格设置Access-Control-Allow-Origin使用同源策略进行保护。
// good
c := cors.New(cors.Options{
AllowedOrigins: []string{"http://qq.com", "https://qq.com"},
AllowCredentials: true,
Debug: false,
})
//引入中间件
handler = c.Handler(handler)
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 1.6 响应输出
# 1.6.1 【必须】设置正确的HTTP响应包类型
- 响应头Content-Type与实际响应内容,应保持一致。如:API响应数据类型是json,则响应头使用
application/json
;若为xml,则设置为text/xml
。
# 1.6.2 【必须】添加安全响应头
- 所有接口、页面,添加响应头
X-Content-Type-Options: nosniff
。 - 所有接口、页面,添加响应头
X-Frame-Options
。按需合理设置其允许范围,包括:DENY
、SAMEORIGIN
、ALLOW-FROM origin
。用法参考:MDN文档 (opens new window)
# 1.6.3【必须】外部输入拼接到HTTP响应头中需进行过滤
- 应尽量避免外部可控参数拼接到HTTP响应头中,如业务需要则需要过滤掉
\r
、\n
等换行符,或者拒绝携带换行符号的外部输入。
# 1.6.4【必须】外部输入拼接到response页面前进行编码处理
- 直出html页面或使用模板生成html页面的,推荐使用
text/template
自动编码,或者使用html.EscapeString
或text/template
对<, >, &, ',"
等字符进行编码。
import(
"html/template"
)
func outtemplate(w http.ResponseWriter,r *http.Request) {
param1 := r.URL.Query().Get("param1")
tmpl := template.New("hello")
tmpl, _ = tmpl.Parse(`{{define "T"}}{{.}}{{end}}`)
tmpl.ExecuteTemplate(w, "T", param1)
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 1.7 会话管理
# 1.7.1【必须】安全维护session信息
- 用户登录时应重新生成session,退出登录后应清理session。
import (
"net/http"
"github.com/gorilla/mux"
"github.com/gorilla/handlers"
)
//创建cookie
func setToken(res http.ResponseWriter, req *http.Request) {
expireToken := time.Now().Add(time.Minute * 30).Unix()
expireCookie := time.Now().Add(time.Minute * 30)
...
cookie := http.Cookie{
Name: "Auth",
Value: signedToken,
Expires: expireCookie, // 过期失效
HttpOnly: true,
Path: "/",
Domain: "127.0.0.1",
Secure: true
}
http.SetCookie(res, &cookie)
http.Redirect(res, req, "/profile", 307)
}
// 删除cookie
func logout(res http.ResponseWriter, req *http.Request) {
deleteCookie := http.Cookie{
Name: "Auth",
Value: "none",
Expires: time.Now()
}
http.SetCookie(res, &deleteCookie)
return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 1.7.2【必须】CSRF防护
- 涉及系统敏感操作或可读取敏感信息的接口应校验
Referer
或添加csrf_token
。
// good
import (
"net/http"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/signup", ShowSignupForm)
r.HandleFunc("/signup/post", SubmitSignupForm)
//使用csrf_token验证
http.ListenAndServe(":8000",
csrf.Protect([]byte("32-byte-long-auth-key"))(r))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.8 访问控制
# 1.8.1【必须】默认鉴权
除非资源完全可对外开放,否则系统默认进行身份认证,使用白名单的方式放开不需要认证的接口或页面。
根据资源的机密程度和用户角色,以最小权限原则,设置不同级别的权限,如完全公开、登录可读、登录可写、特定用户可读、特定用户可写等
涉及用户自身相关的数据的读写必须验证登录态用户身份及其权限,避免越权操作
-- 伪代码 select id from table where id=:id and userid=session.userid
1
2没有独立账号体系的外网服务使用
QQ
或微信
登录,内网服务使用统一登录服务
登录,其他使用账号密码登录的服务需要增加验证码等二次验证
# 1.9 并发保护
# 1.9.1【必须】禁止在闭包中直接调用循环变量
- 在循环中启动协程,当协程中使用到了循环的索引值,由于多个协程同时使用同一个变量会产生数据竞争,造成执行结果异常。
// bad
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var group sync.WaitGroup
for i := 0; i < 5; i++ {
group.Add(1)
go func() {
defer group.Done()
fmt.Printf("%-2d", i) //这里打印的i不是所期望的
}()
}
group.Wait()
}
// good
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var group sync.WaitGroup
for i := 0; i < 5; i++ {
group.Add(1)
go func(j int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in start()")
}
group.Done()
}()
fmt.Printf("%-2d", j) // 闭包内部使用局部变量
}(i) // 把循环变量显式地传给协程
}
group.Wait()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 1.9.2【必须】禁止并发写map
- 并发写map容易造成程序崩溃并异常退出,建议加锁保护
// bad
func main() {
m := make(map[int]int)
//并发读写
go func() {
for {
_ = m[1]
}
}()
go func() {
for {
m[2] = 1
}
}()
select {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1.9.3【必须】确保并发安全
敏感操作如果未作并发安全限制,可导致数据读写异常,造成业务逻辑限制被绕过。可通过同步锁或者原子操作进行防护。
通过同步锁共享内存
// good
var count int
func Count(lock *sync.Mutex) {
lock.Lock()// 加写锁
count++
fmt.Println(count)
lock.Unlock()// 解写锁,任何一个Lock()或RLock()均需要保证对应有Unlock()或RUnlock()
}
func main() {
lock := &sync.Mutex{}
for i := 0; i < 10; i++ {
go Count(lock) //传递指针是为了防止函数内的锁和调用锁不一致
}
for {
lock.Lock()
c := count
lock.Unlock()
runtime.Gosched()//交出时间片给协程
if c > 10 {
break
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 使用
sync/atomic
执行原子操作
// good
import (
"sync"
"sync/atomic"
)
func main() {
type Map map[string]string
var m atomic.Value
m.Store(make(Map))
var mu sync.Mutex // used only by writers
read := func(key string) (val string) {
m1 := m.Load().(Map)
return m1[key]
}
insert := func(key, val string) {
mu.Lock() // 与潜在写入同步
defer mu.Unlock()
m1 := m.Load().(Map) // 导入struct当前数据
m2 := make(Map) // 创建新值
for k, v := range m1 {
m2[k] = v
}
m2[key] = val
m.Store(m2) // 用新的替代当前对象
}
_, _ = read, insert
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- 1.1 输入校验
- 1.1.1【必须】按类型进行数据校验
- 1.2 SQL操作
- 1.2.1【必须】SQL语句默认使用预编译并绑定变量
- 1.3 网络请求
- 1.3.1【必须】资源请求过滤验证
- 1.4 服务器端渲染
- 1.4.1【必须】模板渲染过滤验证
- 1.5 Web跨域
- 1.5.1【必须】跨域资源共享CORS限制请求来源
- 1.6 响应输出
- 1.6.1 【必须】设置正确的HTTP响应包类型
- 1.6.2 【必须】添加安全响应头
- 1.6.3【必须】外部输入拼接到HTTP响应头中需进行过滤
- 1.6.4【必须】外部输入拼接到response页面前进行编码处理
- 1.7 会话管理
- 1.7.1【必须】安全维护session信息
- 1.7.2【必须】CSRF防护
- 1.8 访问控制
- 1.8.1【必须】默认鉴权
- 1.9 并发保护
- 1.9.1【必须】禁止在闭包中直接调用循环变量
- 1.9.2【必须】禁止并发写map
- 1.9.3【必须】确保并发安全