JS `CORS` (跨域资源共享) 配置错误与 `Access-Control-Allow-Origin` 漏洞利用

各位观众老爷,大家好!今天咱们来聊聊JS的CORS,也就是跨域资源共享,以及它那让人头疼的配置错误和可能存在的Access-Control-Allow-Origin漏洞。这玩意儿,说起来简单,但一不小心就容易掉坑里,今天咱就一块儿把这些坑填平了。

一、啥是CORS?为啥要有它?

想象一下,你有个网站www.example.com,想去www.api.com要点数据。浏览器一看,哎哟,这俩域名不一样啊,这是跨域请求,默认情况下,为了安全起见,浏览器会阻止这种请求。 这就是浏览器的同源策略在作祟。

那为啥要有同源策略呢? 简单来说,就是为了防止恶意网站偷偷摸摸地访问你的数据。 假设没有同源策略,恶意网站可以悄悄地在你的浏览器里发起请求,冒充你访问银行网站,然后盗取你的银行信息,想想都可怕。

但是,实际开发中,跨域请求又是不可避免的。 比如,前后端分离的项目,前端域名和后端API域名通常是不一样的。 这时候,就需要CORS来帮忙了。

CORS本质上是一种机制,允许服务器告诉浏览器,哪些来源(域名、协议、端口)可以访问我的资源。 就像是服务器给浏览器开了一张“通行证”,允许特定的域名来拿数据。

二、Access-Control-Allow-Origin:CORS的核心

CORS的核心就在于服务器返回的Access-Control-Allow-Origin响应头。 这个头告诉浏览器,哪些域名是被允许跨域访问的。

它有三种常见的取值方式:

  • *`` (星号):** 这表示允许所有域名进行跨域访问。 简单粗暴,但也很危险,除非你的API是公开的,否则不建议使用。
  • 具体的域名: 例如,Access-Control-Allow-Origin: www.example.com,表示只允许www.example.com这个域名进行跨域访问。
  • null 这个值有点特殊,通常用于file://协议或者data:协议下的请求。 在这些情况下,浏览器的"源"被认为是null。

三、CORS预检请求(Preflight Request)

如果你的跨域请求比较复杂,比如使用了PUTDELETE等非标准的HTTP方法,或者设置了自定义的请求头,浏览器会先发起一个OPTIONS请求,这就是预检请求。

这个OPTIONS请求会询问服务器,是否允许这种跨域请求。 服务器需要在OPTIONS请求的响应中返回以下头:

  • Access-Control-Allow-Origin: 允许的域名。
  • Access-Control-Allow-Methods: 允许的HTTP方法,例如 GET, POST, PUT, DELETE
  • Access-Control-Allow-Headers: 允许的自定义请求头,例如 Content-Type, Authorization
  • Access-Control-Max-Age: 预检请求的缓存时间,单位是秒。

如果服务器没有正确地响应这些头,浏览器就会阻止实际的跨域请求。

四、CORS配置错误:常见的坑

CORS配置错误是导致跨域问题的罪魁祸首。 下面列举一些常见的错误配置:

  1. *`Access-Control-Allow-Origin: ` 滥用:**

    虽然简单,但非常危险。 这相当于把你的API完全暴露给所有人,任何网站都可以访问你的数据。

    风险: 任何恶意网站都可以窃取用户数据、冒充用户操作等。

  2. 忘记处理预检请求:

    如果你的跨域请求需要预检,但服务器没有正确地响应OPTIONS请求,浏览器就会报错。

    错误示例 (Node.js/Express):

    app.options('/api/data', (req, res) => {
        res.header('Access-Control-Allow-Origin', '*'); // 错误:应该根据请求的Origin动态设置
        res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
        res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
        res.send();
    });

    正确示例 (Node.js/Express):

    app.options('/api/data', (req, res) => {
        const origin = req.headers.origin;
        if (allowedOrigins.includes(origin)) { // allowedOrigins是一个允许的域名列表
            res.header('Access-Control-Allow-Origin', origin);
        } else {
            res.header('Access-Control-Allow-Origin', ''); // 不允许的域名,返回空字符串
        }
        res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
        res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
        res.send();
    });
  3. Access-Control-Allow-Credentials 的使用不当:

    如果你的API需要携带cookie等凭证,你需要设置Access-Control-Allow-Credentials: true。 同时,客户端也需要设置withCredentials = true

    错误示例 (服务端):

    res.header('Access-Control-Allow-Origin', '*'); // 错误:使用*时不能设置Access-Control-Allow-Credentials: true
    res.header('Access-Control-Allow-Credentials', 'true');

    正确示例 (服务端):

    const origin = req.headers.origin;
    if (allowedOrigins.includes(origin)) {
        res.header('Access-Control-Allow-Origin', origin); // 必须是具体的域名
        res.header('Access-Control-Allow-Credentials', 'true');
    }

    正确示例 (客户端/JavaScript):

    const xhr = new XMLHttpRequest();
    xhr.withCredentials = true; // 必须设置
    xhr.open('GET', 'https://api.example.com/data');
    xhr.send();

    注意: 如果服务端设置了 Access-Control-Allow-Credentials: true,但客户端没有设置 withCredentials = true,浏览器不会发送cookie。 反之,如果客户端设置了 withCredentials = true,但服务端没有设置 Access-Control-Allow-Credentials: true,请求会被浏览器阻止。

  4. Origin验证不严格:

    有些服务器会根据Origin请求头来动态设置Access-Control-Allow-Origin。 如果验证不严格,可能会被绕过。

    错误示例 (PHP):

    <?php
    $origin = $_SERVER['HTTP_ORIGIN'];
    if (strpos($origin, '.example.com') !== false) {
        header('Access-Control-Allow-Origin: ' . $origin);
    }
    ?>

    风险: 上面的代码只检查了Origin是否包含.example.com,攻击者可以注册一个evil.example.com.attacker.com这样的域名来绕过验证。

    正确示例 (PHP):

    <?php
    $allowedOrigins = array(
        'www.example.com',
        'api.example.com'
    );
    $origin = $_SERVER['HTTP_ORIGIN'];
    if (in_array($origin, $allowedOrigins)) {
        header('Access-Control-Allow-Origin: ' . $origin);
    }
    ?>

五、Access-Control-Allow-Origin漏洞利用:如何被黑?

如果CORS配置不当,攻击者可能会利用这些漏洞来窃取用户数据、执行恶意操作。

  1. Access-Control-Allow-Origin: null 漏洞:

    有些情况下,服务器会错误地返回Access-Control-Allow-Origin: null。 这通常发生在使用了不正确的Origin验证逻辑时。

    攻击场景: 攻击者可以创建一个本地的HTML文件 (使用file://协议) 或者一个data: URL,然后发起跨域请求。 由于这些请求的Originnull,如果服务器错误地允许null作为合法的Origin,攻击者就可以绕过CORS限制。

    代码示例 (攻击者的HTML文件):

    <!DOCTYPE html>
    <html>
    <head>
        <title>CORS Exploit</title>
    </head>
    <body>
        <script>
            const xhr = new XMLHttpRequest();
            xhr.open('GET', 'https://vulnerable.example.com/api/sensitive_data'); // 假设vulnerable.example.com允许Origin: null
            xhr.onload = function() {
                alert(xhr.responseText); // 窃取数据
            };
            xhr.onerror = function() {
                alert('Request failed');
            };
            xhr.send();
        </script>
    </body>
    </html>

    防御: 绝对不要允许Origin: null。 检查你的CORS配置,确保只允许明确的域名。

  2. 子域名接管(Subdomain Takeover)结合CORS:

    如果你的服务器允许某个子域名 (例如*.example.com) 跨域访问,而攻击者成功地接管了一个子域名 (例如evil.example.com),攻击者就可以利用这个子域名来发起跨域请求,窃取数据。

    攻击步骤:

    1. 攻击者找到一个可以被接管的子域名 (例如,一个指向不存在的云存储服务的CNAME记录)。
    2. 攻击者接管这个子域名。
    3. 攻击者在接管的子域名上部署恶意代码,发起跨域请求到api.example.com,窃取数据。

    防御:

    • 定期检查你的DNS记录,确保没有悬空的CNAME记录,防止子域名被接管。
    • 尽可能避免使用通配符域名 (*) 作为Access-Control-Allow-Origin的值。
    • 如果必须使用通配符域名,要仔细评估风险,并采取额外的安全措施。
  3. Access-Control-Allow-Origin 绕过技巧:

    有些老旧的浏览器或者不规范的实现,可能存在一些CORS绕过技巧。 例如,利用URL的片段标识符 (#) 或者查询参数 (?) 来绕过Origin验证。 虽然这些漏洞比较少见,但也需要关注。

六、最佳实践:如何正确配置CORS?

  1. *永远不要使用`Access-Control-Allow-Origin: `,除非你的API是完全公开的。**
  2. 使用白名单来限制允许跨域访问的域名。
  3. 严格验证Origin请求头,防止被绕过。
  4. 正确处理预检请求。
  5. 如果需要携带cookie等凭证,确保同时设置Access-Control-Allow-Credentials: true (服务端) 和 withCredentials = true (客户端)。
  6. 定期审查你的CORS配置,确保没有安全漏洞。
  7. 使用CORS中间件或者库,可以简化CORS配置,并减少出错的可能性。 (例如,Node.js的 cors 模块)
  8. 在开发和测试环境中,可以适当放宽CORS限制,方便调试。 但在生产环境中,一定要严格配置CORS。

七、一些实用的代码示例

1. Node.js (Express) + cors 模块:

const express = require('express');
const cors = require('cors');
const app = express();

const allowedOrigins = ['http://www.example.com', 'http://localhost:3000'];

const corsOptions = {
  origin: function (origin, callback) {
    if (allowedOrigins.indexOf(origin) !== -1 || !origin) { // !origin 用于允许本地开发 (file://)
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'))
    }
  },
  credentials: true // 允许携带cookie
}

app.use(cors(corsOptions));

app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello from API!' });
});

app.listen(4000, () => {
  console.log('Server listening on port 4000');
});

2. Python (Flask):

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": ["http://www.example.com", "http://localhost:3000"]}})

@app.route("/api/data")
def hello_world():
    return {"message": "Hello from API!"}

if __name__ == '__main__':
    app.run(debug=True)

3. Apache (.htaccess):

<IfModule mod_headers.c>
    SetEnvIf Origin "http://www.example.com" AccessControlAllowOrigin=$0
    Header set Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin
    Header set Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE"
    Header set Access-Control-Allow-Headers "Content-Type, Authorization"
    Header set Access-Control-Allow-Credentials "true"
    <FilesMatch ".(ttf|ttc|otf|eot|woff|woff2|font.css|css|js)$">
        <If "-f %{REQUEST_FILENAME}">
            Header set Access-Control-Allow-Origin "*"
        </If>
    </FilesMatch>
    # Handle preflight OPTIONS requests
    <If "%{REQUEST_METHOD} == 'OPTIONS'">
        Header set Access-Control-Max-Age "3600"
        Header set Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE"
        Header set Access-Control-Allow-Headers "Content-Type, Authorization"
        Header set Access-Control-Allow-Credentials "true"
        Header set Access-Control-Allow-Origin "%{HTTP_ORIGIN}e"
        Header set Vary Origin
        Header set Vary Access-Control-Request-Method
        Header set Vary Access-Control-Request-Headers
        <Files *>
            Require all granted
        </Files>
    </If>
</IfModule>

八、总结

CORS是一个重要的安全机制,但配置错误很容易导致安全漏洞。 理解CORS的工作原理,避免常见的错误配置,并定期审查你的CORS设置,才能确保你的API安全可靠。 记住,安全无小事,小心驶得万年船!

好了,今天的讲座就到这里。 各位观众老爷,咱们下回再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注