Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Use case-insensitive comparison for CORS preflight responses (#11511)",
"packageName": "react-native-windows",
"email": "dev@rocha.red",
"dependentChangeType": "patch"
}
49 changes: 49 additions & 0 deletions vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
#include <Networking/WinRTTypes.h>
#include "WinRTNetworkingMocks.h"

// Boost Library
#include <boost/algorithm/string.hpp>

// Windows API
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Http.h>
Expand All @@ -17,6 +20,7 @@ using namespace winrt::Windows::Web::Http;
using Microsoft::React::Networking::OriginPolicyHttpFilter;
using Microsoft::React::Networking::RequestArgs;
using Microsoft::React::Networking::ResponseOperation;
using std::wstring;
using winrt::Windows::Foundation::Uri;

namespace Microsoft::React::Test {
Expand Down Expand Up @@ -252,6 +256,51 @@ TEST_CLASS (OriginPolicyHttpFilterTest) {
}
}

TEST_METHOD(ValidatePreflightResponseHeadersCaseMismatchSucceeds) {
auto mockFilter = winrt::make<MockHttpBaseFilter>();
mockFilter.as<MockHttpBaseFilter>()->Mocks.SendRequestAsync =
[](HttpRequestMessage const &request) -> ResponseOperation {
HttpResponseMessage response{};

response.StatusCode(HttpStatusCode::Ok);
response.Headers().Insert(L"Access-Control-Allow-Origin", L"*");

// Return allowed headers as requested by client, in lower case.
// This tests case-insensitive preflight validation.
auto allowHeaders = boost::to_lower_copy(wstring{request.Headers().Lookup(L"Access-Control-Request-Headers")});
response.Headers().Insert(L"Access-Control-Allow-Headers", std::move(allowHeaders));

co_return response;
};

auto reqArgs = winrt::make<RequestArgs>();
auto request = HttpRequestMessage(HttpMethod::Get(), Uri{L"http://somehost"});
request.Properties().Insert(L"RequestArgs", reqArgs);
request.Headers().TryAppendWithoutValidation(L"ChangeMyCase", L"Value");
// Should implicitly set Conent-Length and Content-Type
request.Content(HttpStringContent{L"PreflightContent"});

auto filter = winrt::make<OriginPolicyHttpFilter>(mockFilter);
auto opFilter = filter.as<OriginPolicyHttpFilter>();

OriginPolicyHttpFilter::SetStaticOrigin("http://somehost");
try {
auto sendOp = opFilter->SendPreflightAsync(request);
sendOp.get();

auto response = sendOp.GetResults();
opFilter->ValidatePreflightResponse(request, response);

OriginPolicyHttpFilter::SetStaticOrigin({});
Assert::IsTrue(boost::iequals(
response.Headers().Lookup(L"Access-Control-Allow-Headers").c_str(),
L"ChangeMyCase, Content-Length, Content-Type"));
} catch (const winrt::hresult_error &e) {
OriginPolicyHttpFilter::SetStaticOrigin({});
Assert::Fail(e.message().c_str());
}
}

TEST_METHOD(ValidatePreflightResponseMainAndContentHeadersSucceeds) {
auto mockFilter = winrt::make<MockHttpBaseFilter>();
mockFilter.as<MockHttpBaseFilter>()->Mocks.SendRequestAsync =
Expand Down
30 changes: 17 additions & 13 deletions vnext/Shared/Networking/OriginPolicyHttpFilter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,26 @@ namespace Microsoft::React::Networking {

#pragma region OriginPolicyHttpFilter

#pragma region ConstWcharComparer
#pragma region CaseInsensitiveComparer

bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, const wchar_t *b) const {
bool OriginPolicyHttpFilter::CaseInsensitiveComparer::operator()(const wchar_t *a, const wchar_t *b) const {
return _wcsicmp(a, b) < 0;
}

#pragma endregion ConstWcharComparer
bool OriginPolicyHttpFilter::CaseInsensitiveComparer::operator()(const wstring &a, const wstring &b) const {
return _wcsicmp(a.c_str(), b.c_str()) < 0;
}

#pragma endregion CaseInsensitiveComparer

// https://fetch.spec.whatwg.org/#forbidden-method
/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::ConstWcharComparer> OriginPolicyHttpFilter::s_forbiddenMethods =
{L"CONNECT", L"TRACE", L"TRACK"};
/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::CaseInsensitiveComparer>
OriginPolicyHttpFilter::s_forbiddenMethods = {L"CONNECT", L"TRACE", L"TRACK"};

/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::ConstWcharComparer>
/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::CaseInsensitiveComparer>
OriginPolicyHttpFilter::s_simpleCorsMethods = {L"GET", L"HEAD", L"POST"};

/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::ConstWcharComparer>
/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::CaseInsensitiveComparer>
OriginPolicyHttpFilter::s_simpleCorsRequestHeaderNames = {
L"Accept",
L"Accept-Language",
Expand All @@ -64,19 +68,19 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co
L"Viewport-Width",
L"Width"};

/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::ConstWcharComparer>
/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::CaseInsensitiveComparer>
OriginPolicyHttpFilter::s_simpleCorsResponseHeaderNames =
{L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma"};

/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::ConstWcharComparer>
/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::CaseInsensitiveComparer>
OriginPolicyHttpFilter::s_simpleCorsContentTypeValues = {
L"application/x-www-form-urlencoded",
L"multipart/form-data",
L"text/plain"};

// https://fetch.spec.whatwg.org/#forbidden-header-name
// Chromium still bans "User-Agent" due to https://crbug.com/571722
/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::ConstWcharComparer>
/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::CaseInsensitiveComparer>
OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNames = {
L"Accept-Charset",
L"Accept-Encoding",
Expand All @@ -99,13 +103,13 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co
L"Upgrade",
L"Via"};

/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::ConstWcharComparer>
/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::CaseInsensitiveComparer>
OriginPolicyHttpFilter::s_cookieSettingResponseHeaders = {
L"Set-Cookie",
L"Set-Cookie2", // Deprecated by the spec, but probably still used
};

/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::ConstWcharComparer>
/*static*/ set<const wchar_t *, OriginPolicyHttpFilter::CaseInsensitiveComparer>
OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = {L"Proxy-", L"Sec-"};

/*static*/ Uri OriginPolicyHttpFilter::s_origin{nullptr};
Expand Down Expand Up @@ -293,7 +297,7 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co
}

/*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues(
winrt::Windows::Foundation::Collections::IMap<hstring, hstring> const &headers) {
IMap<hstring, hstring> const &headers) {
using std::wregex;
using std::wsregex_token_iterator;

Expand Down
23 changes: 12 additions & 11 deletions vnext/Shared/Networking/OriginPolicyHttpFilter.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,30 @@ class OriginPolicyHttpFilter
: public winrt::
implements<OriginPolicyHttpFilter, winrt::Windows::Web::Http::Filters::IHttpFilter, IRedirectEventSource> {
public:
struct ConstWcharComparer {
struct CaseInsensitiveComparer {
bool operator()(const wchar_t *, const wchar_t *) const;
bool operator()(const std::wstring &, const std::wstring &) const;
};

private:
static std::set<const wchar_t *, ConstWcharComparer> s_forbiddenMethods;
static std::set<const wchar_t *, ConstWcharComparer> s_simpleCorsMethods;
static std::set<const wchar_t *, ConstWcharComparer> s_simpleCorsRequestHeaderNames;
static std::set<const wchar_t *, ConstWcharComparer> s_simpleCorsResponseHeaderNames;
static std::set<const wchar_t *, ConstWcharComparer> s_simpleCorsContentTypeValues;
static std::set<const wchar_t *, ConstWcharComparer> s_corsForbiddenRequestHeaderNames;
static std::set<const wchar_t *, ConstWcharComparer> s_corsForbiddenRequestHeaderNamePrefixes;
static std::set<const wchar_t *, ConstWcharComparer> s_cookieSettingResponseHeaders;
static std::set<const wchar_t *, CaseInsensitiveComparer> s_forbiddenMethods;
static std::set<const wchar_t *, CaseInsensitiveComparer> s_simpleCorsMethods;
static std::set<const wchar_t *, CaseInsensitiveComparer> s_simpleCorsRequestHeaderNames;
static std::set<const wchar_t *, CaseInsensitiveComparer> s_simpleCorsResponseHeaderNames;
static std::set<const wchar_t *, CaseInsensitiveComparer> s_simpleCorsContentTypeValues;
static std::set<const wchar_t *, CaseInsensitiveComparer> s_corsForbiddenRequestHeaderNames;
static std::set<const wchar_t *, CaseInsensitiveComparer> s_corsForbiddenRequestHeaderNamePrefixes;
static std::set<const wchar_t *, CaseInsensitiveComparer> s_cookieSettingResponseHeaders;

// NOTE: Assumes static origin through owning client/resource/module/(React) instance's lifetime.
static winrt::Windows::Foundation::Uri s_origin;

struct AccessControlValues {
winrt::hstring AllowedOrigin;
winrt::hstring AllowedCredentials;
std::set<std::wstring> AllowedHeaders;
std::set<std::wstring, CaseInsensitiveComparer> AllowedHeaders;
std::set<std::wstring> AllowedMethods;
std::set<std::wstring> ExposedHeaders;
std::set<std::wstring, CaseInsensitiveComparer> ExposedHeaders;
size_t MaxAge;
};

Expand Down