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:

    访问失败,浏览器未加载出任何内容,报错信息显示跨源请求被阻止,原因是源不匹配:

    image-20240403105651-d05b2qu.png

    但是MITM抓包可以发现,服务器响应了请求,只是浏览器没有显示:

    image-20240403110648-lrl6wqf.png

    image-20240403110657-xcl4anz.png

  • 后端将前端的源添加的允许的源中:

    c.Writer.Header().Set("Access-Control-Allow-Origin", "http://127.0.0.1")​​

    前端访问时成功收到响应:

    image-20240403111140-fy79i25.png

  • PUT

    访问失败,浏览器什么也没有显示,

    image-20240403110802-18dd41o.png

    根据报错信息,PUT请求失败,同时抓包发现没有针对test​路由的请求,服务器可以看到options​请求。也就是说,复杂请求前先进行了options​预检请求,该请求未通过,所以没有后续的复杂请求:

    image-20240403110935-73t2yk6.png
    后端服务器可以看到options请求
    image-20240403112949-i7qkprl.png

  • 同样,在允许的源中添加http://127.0.0.1​后响应成功:

    image-20240403113028-757pq5v.png

    image-20240403113034-28dzr79.png

    image-20240403113045-1cp588u.png

综上,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>​:

出现弹窗:

image-20240404153039-wgwb00b.png

后端服务器增加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​:

image-20240404153651-i1y52ua.png

然后引入这个js文件:<script src="http://127.0.0.1/evil.js"></script>​:

image-20240404153721-ovwnddk.png

出现弹窗,因为这个js文件和服务器时同源的,满足Content-Security-Policy", "default-src 'self'; script-src​的要求,所以可以被执行。

最后修改:2025 年 01 月 10 日
如果觉得我的文章对你有用,请随意赞赏