先提一点,做实例的时候遇到的 cookie
带不上的原因,是谷歌提出的 SameSite Cookies
机制导致的,之前都不知道 cookie
还有这么个属性。
趣事
2011年12月21日,国内最大的开发者社区 CSDN
被黑客在互联网上公布了600万注册用户的数据。更糟糕的是,CSDN
在数据库中明文保存了用户的密码。当然了,这次事故的原理,肯定不是 CSRF
。
原理
CSRF
是 Cross-Site Request Forgery
的缩写,跨站请求伪造。本篇阐述的是基于浏览器 cookie
机制伪造请求的方式。
我们有三个角色:
- 用户 U
- 被攻击的服务器 A
- 攻击者 B
需要满足以下几个条件:
- 用户登录过网站
A
,并且保持登录状态(cookie未清空),同时访问了网站B
- 网站
A
对于自己开放的修改信息的接口,只做了cookie
的校验(现在应该是很少有网站会这么做了)。 - 网站
B
提前了解了网站A
的相应接口。
过程中遇到的一些问题
cookie
有关 cookie
的介绍,网上资源很多,这里就不详细介绍了,只说以下 SameSite
属性。
cookie
的 SameSite
的出现,也是由于安全原因考虑,各大浏览器厂商也重视了该问题,same-site cookies
是基于 Chrome
和 Mozilla
开发者花了三年多时间制定的 IETF
标准。它是在原有的 Cookie
中,新添加了一个 SameSite
属性,它标识着在非同源的请求中,是否可以带上 Cookie
,它可以设置为3个值,分别为:
- Strict
Strict
是最严格的,它完全禁止在跨站情况下,发送Cookie
。只有在自己的网站内部发送请求,才会带上Cookie
。不过这个规则过于严格,会影响用户的体验。 - Lax
默认的规则。Lax
的规则稍稍放宽了些,大部分跨站的请求也不会带上Cookie,但是一些导航的Get请求会带上Cookie,如下: - None
None
就是关闭SameSite
属性,所有的情况下都发送Cookie
。不过SameSite
设置None
,还要同时设置Cookie
的Secure
属性,否则是不生效的(下图为手动设置的SameSite None
,红色警告了)。
Cors 跨域
期间也遇到了跨域相关的问题,可以在服务器端进行相应的配置,具体原理自行搜取。
本服务使用的是 go
的 gin
框架,可以直接使用第三方跨域库 github.com/rs/cors/wrapper/gin
,如下:
import (
cors "github.com/rs/cors/wrapper/gin"
"github.com/gin-gonic/gin"
)
func ServerA() {
router := gin.Default()
router.Use(cors.Default())
router.Run(":9090")
}
源码实现
相应的源码在 Gitee/GoTest 下的 CSRF
文件夹内,感兴趣的可以下载源码测试。
案例:用户登录 Server A
(部署在:172.16.60.43:9090),这个时候收到了一封邮件(是兄弟就来砍我…),点击打开了诈骗网站 B
(部署在本地)。
主要就是验证:我们从 B
网站访问 A
的时候,会不会带上 cookie
值,简化的代码如下:
Server A
package main
import (
"commonTest/utils"
"fmt"
"github.com/gin-gonic/gin"
"os"
"path"
//cors "github.com/rs/cors/wrapper/gin"
"net/http"
"strings"
)
// 需要做 cookie 验证
func main() {
router := gin.Default()
//router.Use(cors.Default())
router.Use(customCors())
getwd, err := os.Getwd()
utils.CheckErr(err)
htmlFile := path.Join(getwd, "CSRF", "indexA.html")
fmt.Println("htmlFile: ", htmlFile)
router.LoadHTMLFiles(htmlFile)
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "indexA.html", nil)
})
router.GET("/login", func(c *gin.Context) {
fmt.Println(c.ClientIP())
c.SetCookie("test", "zzyy", 24*60*60, "", "", false, false)
c.String(http.StatusOK, "OK")
})
router.GET("/change", func(c *gin.Context) {
// 只需要验证收到 cookie 信息即可
cookie, err := c.Cookie("test")
if err != nil {
fmt.Println("接收cookie出错了: ", err.Error())
c.String(http.StatusBadRequest, "就你还想来攻击我")
} else {
fmt.Println("成功接收到cookie: ", cookie)
c.String(http.StatusOK, "修改成功")
}
})
err = router.Run(":9090")
if err != nil {
fmt.Println(err)
}
fmt.Println("exit")
}
var allowedMethods = []string{
"POST",
"GET",
"OPTIONS",
"PUT",
"PATCH",
"DELETE",
}
func customCors() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
fmt.Println("origin: ", origin)
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Credentials", "true")
if c.Request.Method == "OPTIONS" {
c.Writer.Header().Set(
"Access-Control-Allow-Methods",
strings.Join(allowedMethods, ", "),
)
c.Writer.Header().Set(
"Access-Control-Allow-Headers",
c.Request.Header.Get("Access-Control-Request-Headers"),
)
}
}
}
indexA.html
<html>
<body>
<h1>登录</h1>
<form action="/login" method="get">
<input type="text" name="account"/>
<input type="password" name="password"/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
提供了两个接口:
/login
用于登录,设置cookie
值/change
用于测试是否收到了cookie
值
访问 http://172.16.60.43:9090/
跳转到登录页面
登录成功,可以看到 cookie
已经赋值
Server B
package CSRF
import (
"commonTest/utils"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"os"
"path"
)
go:embed indexB.html
//var htmlB embed.FS
func ServerB() {
router := gin.Default()
//fInfo, err := fs.Stat(htmlB, "indexB.html")
//utils.CheckErr(err)
//fmt.Println("fInfo: ", fInfo.Name())
//matches, err := fs.Glob(htmlB, "*.html")
//utils.CheckErr(err)
//fmt.Println("matches: ", matches)
//router.LoadHTMLFiles("indexB.html")
// 获取文件所在位置
getwd, err := os.Getwd()
utils.CheckErr(err)
htmlFile := path.Join(getwd, "CSRF", "indexB.html")
fmt.Println("htmlFile: ", htmlFile)
router.LoadHTMLFiles(htmlFile)
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "indexB.html", nil)
})
router.Run(":9092")
}
indexB.html
<html>
<body>
<div>
<form action="http://172.16.60.43:9090/change" method="get">
<input type="hidden" name="account"/>
<input type="hidden" name="password" value="34324"/>
<input type="submit" value="是兄弟就来砍我,一刀99999暴击"/>
</form>
</div>
</body>
</html>
隐藏了用户名密码的输入框,引诱点击提交按钮。
经测试,是能够带上 cookie
请求相应的数据的。
总结
CSRF
的原理,了解了浏览器的 cookie
机制,基本上理解起来问题不大,就是利用了服务器的基于 cookie
的鉴权机制。
web
安全,是个很大的领域,这个例子也让自己过了把当黑客的瘾,找了几个网站看了下,都没成功,看样子大家对 CSRF
攻击都做了功课了。当然这是最基础的 web
攻防,感兴趣的站友们可以自行深入了解。
参考
CSRF的原理与防御 | 你想不想来一次CSRF攻击?
Web安全之CSRF实例解析
OWASP Cheat Sheet Series Cross-Site Request Forgery Prevention
前端网络安全必修 1 同源策略和CSRF