SOP and CORS
首先,执行CORS、SOP和CSP的对主体浏览器,现在主流的浏览器都支持这些功能,服务器需要做的只是设置相应的Header即可。
SOP
即同源策略,只有俩URL的协议、端口、主机完全相同时,才会被认为是同源的,不同源的数据交互默认被限制,以增加安全性,任何跨源请求都需要经过CORS处理。
CORS
即跨域资源共享,用于设置API可以被哪些源访问。
CORS的机制与请求相关:
-
简单请求
,即GET、POST、HEAD
请求会被发送到后端服务器并处理,随后浏览器根据返回包中的Access-Control-Expose-Headers
字段判断自己在不在允许的源,如果不在,则不显示返回包并抛出错误。 -
复杂请求
:除简单请求外的其他请求,都会触发一次预检请求
,使用options
方法请求服务器以查看服务器是否允许发起此次复杂请求,若不允许则不发送请求(如不满足同源条件)。
我们配置Golang+gin作为后端服务器来说明以上内容。
简单举例
测试路由,设置
POST
和PUT
类型的请求,也就是简单请求和复杂请求:r.POST("/test", Test) r.PUT("/test", Test) func Test(c *gin.Context) { var requestData map[string]interface{} //tem := c.Query("key") if err := c.BindJSON(&requestData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } c.String(http.StatusOK, requestData["key"].(string)) } //添加CORS头部: func setMidHeader(r *gin.Engine) { r.Use(func(c *gin.Context) { if c.Request.Method == "OPTIONS" { c.Writer.Header().Set("Access-Control-Allow-Origin", "http://127.0.0.1") // 允许的源 c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, PUT") // 服务器支持什么跨源请求方法 c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") // 允许跨源请求携带哪些请求头 c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length") // 允许浏览器访问的响应头列表 c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") // 允许请求中发送凭据 c.Writer.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' https://trustedscripts.example.com; img-src 'self' https://trustedimages.example.com;") c.AbortWithStatus(http.StatusNoContent) // 截获处理,并响应成功 } c.Writer.Header().Set("Access-Control-Allow-Origin", "http://127.0.0.1") // 允许的源 c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, PUT") // 服务器支持什么跨源请求方法 c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") // 允许跨源请求携带哪些请求头 c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length") // 允许浏览器访问的响应头列表 c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") // 允许请求中发送凭据 c.Writer.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' https://trustedscripts.example.com; img-src 'self' https://trustedimages.example.com;") }) }
前端代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CORS Test</title> <script> // JavaScript fetch API 来发起跨域POST请求 fetch('http://127.0.0.1:9090/test', { method: 'POST', // 指定请求方法 headers: { 'Content-Type': 'application/x-www-form-urlencoded', // 设置内容类型 }, body: JSON.stringify({ key: 'value' }) // 发送的数据 }) .then(response => response.text()) .then(data => console.log(data)) .catch(error => console.error('CORS Error:', error)); </script> </head> <body> <h1>CORS Test Page</h1> </body> </html>
随后我们开始抓包并测试:
POST:
访问失败,浏览器未加载出任何内容,报错信息显示跨源请求被阻止,原因是源不匹配:
但是MITM抓包可以发现,服务器响应了请求,只是浏览器没有显示:
后端将前端的源添加的允许的源中:
c.Writer.Header().Set("Access-Control-Allow-Origin", "http://127.0.0.1")
前端访问时成功收到响应:
PUT
访问失败,浏览器什么也没有显示,
根据报错信息,PUT请求失败,同时抓包发现没有针对
test
路由的请求,服务器可以看到options
请求。也就是说,复杂请求前先进行了options
预检请求,该请求未通过,所以没有后续的复杂请求:
后端服务器可以看到options
请求同样,在允许的源中添加
http://127.0.0.1
后响应成功:
综上,Gin作为后端时要手动处理OPTIONS
请求以适应复杂请求,简单请求会触发后端API调用,可以被抓包,但是浏览器方面会抛出CORS错误,阻止用户访问其响应内容。而复杂请求会有一个预检请求,浏览器检查通过后才会发出复杂请求。
CSP
CSP主要目标为减少和报告XSS攻击,通过指定有效域,即浏览器只执行来自于信任源的脚本,来防止恶意脚本执行,极端情况下,也可以完全禁止脚本执行。
CSP的使用,只要在标头中指定Content-Security-Policy
并编写策略即可。
Content-Security-Policy: [策略]
简单举例
不添加CSP的情况下,页面源码:
<?php
// 处理表单提交
//header("Content-Security-Policy: default-src 'self'; script-src 'self';");
$userInput = '';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// 获取用户输入,注意这里没有进行任何的转义处理
$userInput = $_POST['userInput'];
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>XSS Vulnerable Form</title>
</head>
<body>
<h1>Vulnerable Form</h1>
<form action="<?php echo $_SERVER["PHP_SELF"]; ?>" method="post">
<input type="text" name="userInput" placeholder="Enter some text" />
<button type="submit">Submit</button>
</form>
<!-- 直接输出用户输入,存在XSS风险 -->
<p>You entered: <?php echo $userInput; ?></p>
</body>
</html>
输入恶意payload<script>alert("XSS")</script>
:
出现弹窗:
后端服务器增加CSP:
c.Writer.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' https://trustedscripts.example.com; img-src 'self' https://trustedimages.example.com;")
同时前端也增加相应Header:
<?php
// 处理表单提交
header("Content-Security-Policy: default-src 'self'; script-src 'self';");
$userInput = '';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// 获取用户输入,注意这里没有进行任何的转义处理
$userInput = $_POST['userInput'];
}
?>
再次输入恶意payload,会发现并没有出现弹窗。
此时如果f12查看页面结构,可以发现恶意标签依然在HTML文档里,但是并没有被渲染,因为CSP设置里,script-src
的设置只有self
,意味着只有同源的js代码才会被执行。
所以我们在script-src
中加入http://127.0.0.1
,然后在根目录放入evil.js
:
然后引入这个js文件:<script src="http://127.0.0.1/evil.js"></script>
:
出现弹窗,因为这个js文件和服务器时同源的,满足Content-Security-Policy", "default-src 'self'; script-src
的要求,所以可以被执行。