关于 CORS 的一个问题,大家怎么看

2020-09-02 17:28:15 +08:00
 mitu9527

昨天先去了解了下同源策略,然后又去看了一些和 CORS 相关的文章,几乎所有的文章都告诉我只要在响应头中加入几个 Access-Control-* 头部就可以了。比如像这样:

header('Access-Control-Allow-Origin:http://www.startphp.cn');
header('Access-Control-Allow-Methods:POST');
header('Access-Control-Allow-Headers:x-requested-with, content-type'); 

这段代码是我从别的地方复制过来的,不同文章的具体代码不一样,但都是差不多的,只不过多几个头部或者某个头部的值不一样罢了,没什么本质区别。

服务端这样做确实可以通知客户端,让其拒绝服务端返回的响应,但是服务端关于 CORS 的全部工作就这些么?我觉得不是!至少还应该加一些处理,起码应该在适当的时候结束程序的执行,如果服务端都准备告诉客户端拒绝自己的响应了,那发完响应头部,就没必要再执行后续的业务逻辑了,不是么?反过来说,所有的逻辑都执行完了,然后你再加几个头部去告诉客户端把自己拒绝掉,这不是多此一举么?

我知道客户端只要不发送 Origin 头部,就可以绕过服务端的 CORS 处理;我也知道要想提升安全性,必须得做鉴权才行,但就是觉得服务端的 CORS 处理这块还是应该稍微完善一点,不应该只是加几个头部就草草的结束了才对。如果你觉得我的这种想法是有问题且多余的,欢迎留言指正!

最后我列出我的一点代码,代码并没有做过测试,也更没在开发环境和生产环境中使用过,只是想说明一下我觉得完善的 CORS 处理大概应该是什么样子。具体的逻辑主要参考的是阮一峰的这篇文章:《跨域资源共享 CORS 详解》 。有兴趣的伙伴可以去看看。

<?php

// CORS 配置
$config = [
    'allow_origin' => ["http://b.example.com", "http://c.example.com"],
    'allow_method' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    'allow_headers' => ['X-Custom-Header', 'Accept'],
    'expose_headers' => ['Content-Range'],
    'allow_credentials' => true,
    'max_age' => 86400,
];

// CORS 预检请求
if (isCorsPreflightRequest()) {
    // 检查预检请求
    if (checkCorsPreflightRequest($config)) {
        // 通过
        addCorsPreflightRequestHeaders($config);
    } else {
        // 未通过,什么也不做
    }
    // 退出,不进入业务逻辑部分
    exit;
}

// CORS 简单请求
if (isCorsSimpleRequest()) {
    // 检查简单请求
    if (checkCorsSimpleRequest($config)) {
        // 通过
        addCorsSimpleRequestHeaders($config);
    } else {
        // 没通过,退出,不进入业务逻辑部分
        exit;
    }
}

function isCorsPreflightRequest(): bool
{
    return isset($_SERVER['HTTP_ORIGIN'], $_SERVER['REQUEST_METHOD']) &&
        'OPTIONS' === strtoupper($_SERVER['REQUEST_METHOD']);
}

function checkCorsPreflightRequest(array $config): bool
{
    return checkCorsOrigin($config) && checkCorsRequestMethod($config) && checkCorsRequestHeaders($config);
}

function checkCorsOrigin(array $config): bool
{
    return '*' === $config['allow_origin'] || in_array($_SERVER['HTTP_ORIGIN'], $config['allow_origin'], true);
}

function checkCorsRequestMethod(array $config): bool
{
    if (
        isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) &&
        !in_array(strtoupper($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']), $config['allow_method'], true)
    ) {
        return false;
    }
    return true;
}

function checkCorsRequestHeaders(array $config): bool
{
    if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
        $requestHeaders = explode(',', $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
        $allowHeaders = array_map(fn($header) => strtoupper($header), $config['allow_headers']);
        foreach ($requestHeaders as $requestHeader) {
            $requestHeader = strtoupper(trim($requestHeader));
            if (!in_array($requestHeader, $allowHeaders, true)) {
                return false;
            }
        }
    }
    return true;
}

function addCorsPreflightRequestHeaders(array $config): void
{
    if ('*' === $config['allow_origin']) {
        header('Access-Control-Allow-Origin: *');
    } else {
        header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
    }
    header('Access-Control-Allow-Methods: ' . implode(',', $config['allow_method']));
    header('Access-Control-Allow-Headers: ' . implode(',', $config['allow_headers']));
    header('Access-Control-Allow-Credentials: ' . ($config['allow_credentials'] ? 'true' : 'false'));
    header('Access-Control-Max-Age: ' . $config['max_age']);
}

function isCorsSimpleRequest(): bool
{
    return isset($_SERVER['HTTP_ORIGIN']);
}

function checkCorsSimpleRequest(array $config): bool
{
    return checkCorsOrigin($config);
}

function addCorsSimpleRequestHeaders(array $config, array $exposeHeaders = []): void
{
    if ('*' === $config['allow_origin']) {
        header('Access-Control-Allow-Origin: *');
    } else {
        header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
    }
    if (empty($exposeHeaders)) {
        header('Access-Control-Allow-Headers: ' . implode(',', $config['expose_headers']));
    } else {
        header('Access-Control-Allow-Headers: ' . implode(',', $exposeHeaders));
    }
    header('Access-Control-Allow-Credentials: ' . ($config['allow_credentials'] ? 'true' : 'false'));
}

// 假设下方是业务逻辑部分的代码
// 省略...

3413 次点击
所在节点    程序员
46 条回复
ChanKc
2020-09-02 20:52:27 +08:00
阮一峰就很多地方说不通
我前几天面试也被问简单请求,当时因为是远程的,顺手打开 spec,一搜 simple 啥都没有,直接就说我不会
如果要按是否 preflight 来分,spec 说

A request has an associated use-CORS-preflight flag. Unless stated otherwise, it is unset.

The use-CORS-preflight flag being set is one of several conditions that results in a CORS-preflight request. The use-CORS-preflight flag is set if either one or more event listeners are registered on an XMLHttpRequestUpload object or if a ReadableStream object is used in a request.

If the CORS-preflight flag is set and one of these conditions is true:

There is no method cache entry match for request’s method using request, and either request’s method is not a CORS-safelisted method or request’s use-CORS-preflight flag is set.

There is at least one item in the CORS-unsafe request-header names with request’s header list for which there is no header-name cache entry match using request.

所以除了那些请求头和请求方法以外要考虑的还有是否有缓存(很重要)。另外有 preflight 了不代表普通的 CORS 请求不需要校验,不需要那几个头,只要是 HTTP fetch,都要执行下面两步



Set response and actualResponse to the result of performing an HTTP-network-or-cache fetch using request.

If request’s response tainting is "cors" and a CORS check for request and response returns failure, then return a network error.

response tainting 是 cors 的都要去检查 HTTP 头

至于什么“兼容表单”也是有点问题。表单提交的请求模式是 navigate,对应的 response tainting 是 basic,所以根本不做 CORS check,自然也就用不上 preflight
mitu9527
2020-09-02 21:10:49 +08:00
@no1xsyzy @ChanKc 可能我在内容里面说的“完善”确离“完善”还差很远,不过我的本意也不是想说的代码写的多好,多完善,我是想说的重点是提前 exit 不去执行下面的业务逻辑。回头我去看下规范或者看看别的库和框架是怎么实现的吧。
baobao1270
2020-09-02 22:05:02 +08:00
额,CROS 的本质是浏览器防止 XSS 对客户端攻击的,我个人认为服务器只要支持好预检请求即可。服务端应另行对客户端请求合法性进行校验,因为一个不进行 CROS 检查的客户端可以直接构造请求。
no1xsyzy
2020-09-03 11:03:16 +08:00
@ChanKc ……简单请求不是 CORS 标准的用词,而是 MDN 描述 “不需要预检” 的情况的用词。同时,“这些跨域请求与浏览器发出的其他跨域请求并无二致。”
至于兼容表单…… 按照 fetch standard,原句表示的不是简单请求 “兼容表单”,而是指 “任何比 <form> 侵入性更强的操作均需要预检请求”
For requests that are more involved than what is possible with HTML’s form element, a CORS-preflight request is performed, to ensure request’s current URL supports the CORS protocol.
应当说表单是一个朴素的侵入程度分界线。
no1xsyzy
2020-09-03 11:12:21 +08:00
@mitu9527 因为框架导致忽略了这一个问题吧
比如 flask-cors 可以进行全局覆盖,无视 path,任何 OPTIONS 均作同一处理。
一些 nginx 实现方式也是类似的。
那连上面这么多 is... check... addHeader... 都是不需要的了,直接开始写业务代码就成。
ChanKc
2020-09-03 11:32:02 +08:00
@no1xsyzy 所以我说阮的表述是有些误导性的

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/703603

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX