1、安全基础:OWASP Top 10 解读、常见Web攻击(SQL注入、XSS、CSRF)、Go中的安全编码规范
各位同学,咱们今天聊聊安全基础。说实话,安全这东西,很多人觉得是运维的事,或者等保测评才需要关心。但我个人的经验是,安全应该从写第一行代码就开始。你想想看,一个接口上线后被人注入了,那可不是改几行代码能解决的,搞不好要背锅的。
1.1 OWASP Top 10 快速解读
OWASP Top 10 是什么?说白了,就是全球安全社区公认的「十大最危险的Web漏洞」。我每年都会刷一遍这个榜单,看看有没有新面孔。2021年的版本里,有几个点特别值得关注:
| 排名 | 漏洞类型 | 我的理解 |
|---|---|---|
| A01 | 失效的访问控制 | 用户能访问他不该看的数据,比如普通用户看管理员后台 |
| A02 | 加密机制失效 | 密码明文存储、HTTPS没配好,这些我都见过 |
| A03 | 注入攻击 | SQL注入、命令注入,老牌漏洞但依然常见 |
| A07 | 身份认证失效 | 弱密码、Session固定攻击,嗯,这里要注意 |
我个人习惯是,每次评审代码时,先对照这个列表过一遍。尤其是A01失效的访问控制,我在项目中遇到过好几次——用户明明没权限,但因为路由写漏了校验,数据就泄露了。
1.2 常见Web攻击:SQL注入
SQL注入,老生常谈了。但为什么到现在还有?因为总有人图省事,直接拼接SQL字符串。
// 错误示范:直接拼接用户输入
func getUser(db *sql.DB, id string) (*User, error) {
query := "SELECT * FROM users WHERE id = '" + id + "'"
rows, err := db.Query(query)
// ...
}
你想想看,如果用户传入 1' OR '1'='1,那查询就变成了 SELECT * FROM users WHERE id = '1' OR '1'='1',所有用户数据全出来了。我曾经在一个老项目里看到过这种代码,当时后背一凉。
正确的做法是什么?参数化查询。Go里用 database/sql 包时,一定要用占位符:
// 正确做法:使用参数化查询
func getUser(db *sql.DB, id string) (*User, error) {
query := "SELECT * FROM users WHERE id = ?"
row := db.QueryRow(query, id)
// ...
}
? 占位符是数据库驱动的标准写法。如果你用PostgreSQL,记得换成 $1、$2 这种。
1.3 常见Web攻击:XSS(跨站脚本攻击)
XSS攻击,说白了就是攻击者往你的页面里塞了一段恶意脚本。比如用户在评论区写了 <script>alert('你被黑了')</script>,如果你的后端直接把这个内容渲染到页面上,那所有看这条评论的用户都会中招。
我在项目中遇到过一种情况:一个内部管理系统,用户名字段没做转义,结果有人把用户名改成了 <img src=x onerror=alert(1)>,每次鼠标悬停就弹窗。嗯,那段时间运维同学快疯了。
Go里怎么防?模板引擎默认是转义的,但你要确保别用 template.HTML 这种类型去输出用户输入:
// 安全做法:使用模板引擎的默认转义
tmpl := template.Must(template.New("example").Parse("Hello, {{.Name}}"))
tmpl.Execute(w, map[string]interface{}{
"Name": "<script>alert('xss')</script>",
})
// 输出:Hello, <script>alert('xss')</script>
html/template 包是你的好朋友。
1.4 常见Web攻击:CSRF(跨站请求伪造)
CSRF攻击,简单说就是:用户在你网站登录了,然后不小心点了个恶意链接,那个链接就冒充用户去执行操作。比如转账、改密码。
为什么会这样?因为浏览器会自动带上Cookie。你想想看,如果用户登录了银行网站,然后去逛了一个恶意论坛,论坛里有个隐藏的 <img src="http://bank.com/transfer?to=attacker&amount=10000">,那浏览器就会带着用户的Cookie去请求这个地址。
Go里怎么防?我建议用CSRF Token。每个表单都生成一个随机Token,存在Session里,提交时校验。Gin框架有现成的中间件:
// 使用gin-contrib/csrf中间件
r := gin.Default()
r.Use(csrf.Middleware(csrf.Options{
Secret: "your-secret-key",
ErrorFunc: func(c *gin.Context) {
c.String(400, "CSRF token mismatch")
c.Abort()
},
}))
1.5 Go中的安全编码规范
好了,理论说完了,咱们来点实际的。Go语言本身是内存安全的,但业务逻辑上的漏洞还得靠开发者自己注意。我总结了几条我自己的编码习惯:
- 永远不要拼接SQL —— 用
database/sql的参数化查询,或者ORM(比如GORM) - 输出到HTML前必须转义 —— 用
html/template,别用text/template - 敏感信息不要硬编码 —— 密钥、密码用环境变量或配置中心
- 限制请求体大小 —— 防止大文件上传导致内存溢出
- 使用HTTPS —— 别让数据在网络上裸奔
这里给一个简单的中间件示例,用来限制请求体大小:
// 限制请求体为10MB
func limitRequestBody(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 10<<20) // 10MB
next.ServeHTTP(w, r)
})
}
最后说一句:安全不是一锤子买卖。你写代码时多花5分钟做安全校验,可能就避免了一次线上事故。我曾经因为少写一个参数校验,导致生产环境被扫出SQL注入漏洞,那周加班到凌晨三点。嗯,从那以后我再也不敢偷懒了。