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": "Make HTTP origin value an instance member (#12821)",
"packageName": "react-native-windows",
"email": "julio.rocha@microsoft.com",
"dependentChangeType": "patch"
}
53 changes: 48 additions & 5 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,48 @@ 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 Content-Length and Content-Type
request.Content(HttpStringContent{L"PreflightContent"});

auto filter = winrt::make<OriginPolicyHttpFilter>("http://somehost", mockFilter);
auto opFilter = filter.as<OriginPolicyHttpFilter>();

try {
auto sendOp = opFilter->SendPreflightAsync(request);
sendOp.get();

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

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) {
Assert::Fail(e.message().c_str());
}
}

TEST_METHOD(ValidatePreflightResponseMainAndContentHeadersSucceeds) {
auto mockFilter = winrt::make<MockHttpBaseFilter>();
mockFilter.as<MockHttpBaseFilter>()->Mocks.SendRequestAsync =
Expand All @@ -271,26 +317,23 @@ TEST_CLASS (OriginPolicyHttpFilterTest) {
auto request = HttpRequestMessage(HttpMethod::Get(), Uri{L"http://somehost"});
request.Properties().Insert(L"RequestArgs", reqArgs);
request.Headers().TryAppendWithoutValidation(L"Authorization", L"Bearer abc");
// Should implicitly set Conent-Length and Content-Type
// Should implicitly set Content-Length and Content-Type
request.Content(HttpStringContent{L"PreflightContent"});

auto filter = winrt::make<OriginPolicyHttpFilter>(mockFilter);
auto filter = winrt::make<OriginPolicyHttpFilter>("http://somehost", 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::AreEqual(
L"Authorization, Content-Length, Content-Type",
response.Headers().Lookup(L"Access-Control-Allow-Headers").c_str());
} catch (const winrt::hresult_error &e) {
OriginPolicyHttpFilter::SetStaticOrigin({});
Assert::Fail(e.message().c_str());
}
}
Expand Down
72 changes: 36 additions & 36 deletions vnext/Shared/Networking/OriginPolicyHttpFilter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <regex>

using std::set;
using std::string;
using std::wstring;

using winrt::hresult_error;
Expand All @@ -37,22 +38,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 +69,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,24 +104,15 @@ 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};

/*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string &&url) {
if (!url.empty())
s_origin = Uri{to_hstring(url)};
else
s_origin = nullptr;
}

/*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept {
return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port();
}
Expand Down Expand Up @@ -301,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 Expand Up @@ -381,10 +377,14 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co
}
}

OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const &innerFilter) : m_innerFilter{innerFilter} {}
OriginPolicyHttpFilter::OriginPolicyHttpFilter(string &&origin, IHttpFilter const &innerFilter)
: m_origin{nullptr}, m_innerFilter{innerFilter} {
if (!origin.empty())
m_origin = Uri{to_hstring(origin)};
}

OriginPolicyHttpFilter::OriginPolicyHttpFilter()
: OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {}
OriginPolicyHttpFilter::OriginPolicyHttpFilter(string &&origin)
: OriginPolicyHttpFilter(std::move(origin), winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {}

OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &request) {
auto effectiveOriginPolicy =
Expand All @@ -394,17 +394,17 @@ OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &r
return effectiveOriginPolicy;

case OriginPolicy::SameOrigin:
if (!IsSameOrigin(s_origin, request.RequestUri()))
if (!IsSameOrigin(m_origin, request.RequestUri()))
throw hresult_error{E_INVALIDARG, L"SOP (same-origin policy) is enforced"};
break;

case OriginPolicy::SimpleCrossOriginResourceSharing:
// Check for disallowed mixed content
if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") &&
s_origin.SchemeName() != request.RequestUri().SchemeName())
m_origin.SchemeName() != request.RequestUri().SchemeName())
throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"};

if (IsSameOrigin(s_origin, request.RequestUri()))
if (IsSameOrigin(m_origin, request.RequestUri()))
// Same origin. Therefore, skip Cross-Origin handling.
effectiveOriginPolicy = OriginPolicy::SameOrigin;
else if (!IsSimpleCorsRequest(request))
Expand All @@ -420,7 +420,7 @@ OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &r
// Example: On the Edge browser, an XHR request with the "Host" header set gets rejected as unsafe.
// https://fetch.spec.whatwg.org/#forbidden-header-name

if (s_origin.SchemeName() != request.RequestUri().SchemeName())
if (m_origin.SchemeName() != request.RequestUri().SchemeName())
throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"};

if (!AreSafeRequestHeaders(request.Headers()))
Expand All @@ -429,7 +429,7 @@ OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &r
if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend())
throw hresult_error{E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing"};

if (IsSameOrigin(s_origin, request.RequestUri()))
if (IsSameOrigin(m_origin, request.RequestUri()))
effectiveOriginPolicy = OriginPolicy::SameOrigin;
else if (IsSimpleCorsRequest(request))
effectiveOriginPolicy = OriginPolicy::SimpleCrossOriginResourceSharing;
Expand Down Expand Up @@ -466,7 +466,7 @@ void OriginPolicyHttpFilter::ValidateAllowOrigin(
// 4.10.4 - Mismatched allow origin
auto taintedOriginProp = props.TryLookup(L"TaintedOrigin");
auto taintedOrigin = taintedOriginProp && winrt::unbox_value<bool>(taintedOriginProp);
auto origin = taintedOrigin ? nullptr : s_origin;
auto origin = taintedOrigin ? nullptr : m_origin;
if (allowedOrigin.empty() || !IsSameOrigin(origin, Uri{allowedOrigin})) {
hstring errorMessage;
if (allowedOrigin.empty())
Expand Down Expand Up @@ -597,7 +597,7 @@ void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const &respons
bool originAllowed = false;
for (const auto &header : response.Headers()) {
if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) {
originAllowed |= L"*" == header.Value() || s_origin == Uri{header.Value()};
originAllowed |= L"*" == header.Value() || m_origin == Uri{header.Value()};
}
}

Expand Down Expand Up @@ -685,7 +685,7 @@ ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage
}

preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames);
preflightRequest.Headers().Insert(L"Origin", GetOrigin(s_origin));
preflightRequest.Headers().Insert(L"Origin", GetOrigin(m_origin));
preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS");

co_return {co_await m_innerFilter.SendRequestAsync(preflightRequest)};
Expand All @@ -702,7 +702,7 @@ bool OriginPolicyHttpFilter::OnRedirecting(
// origin=http://a.com. Since the origin matches the URL, the request is authorized at http://a.com, but it actually
// allows http://b.com to bypass the CORS check at http://a.com since the redirected URL is from http://b.com.
if (!IsSameOrigin(response.Headers().Location(), request.RequestUri()) &&
!IsSameOrigin(s_origin, request.RequestUri())) {
!IsSameOrigin(m_origin, request.RequestUri())) {
// By masking the origin field in the request header, we make it impossible for the server to set a single value for
// the access-control-allow-origin header. It means, the only way to support redirect is that server allows access
// from all sites through wildcard.
Expand Down Expand Up @@ -734,7 +734,7 @@ ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage co
// Allow only HTTP or HTTPS schemes
if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" &&
coRequest.RequestUri().SchemeName() != L"http")
throw hresult_error{E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]"};
throw hresult_error{E_INVALIDARG, L"Invalid URL scheme: [" + m_origin.SchemeName() + L"]"};

if (!GetRuntimeOptionBool("Http.OmitCredentials")) {
coRequest.Properties().Lookup(L"RequestArgs").as<RequestArgs>()->WithCredentials = false;
Expand Down Expand Up @@ -771,7 +771,7 @@ ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage co

if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing ||
originPolicy == OriginPolicy::CrossOriginResourceSharing) {
coRequest.Headers().Insert(L"Origin", GetOrigin(s_origin));
coRequest.Headers().Insert(L"Origin", GetOrigin(m_origin));
}

auto response = co_await m_innerFilter.SendRequestAsync(coRequest);
Expand Down
34 changes: 16 additions & 18 deletions vnext/Shared/Networking/OriginPolicyHttpFilter.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,37 +22,35 @@ 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;

// NOTE: Assumes static origin through owning client/resource/module/(React) instance's lifetime.
static winrt::Windows::Foundation::Uri s_origin;
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;

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;
};

winrt::Windows::Foundation::Uri m_origin;

winrt::Windows::Web::Http::Filters::IHttpFilter m_innerFilter;

public:
static void SetStaticOrigin(std::string &&url);

static bool IsSameOrigin(
winrt::Windows::Foundation::Uri const &u1,
winrt::Windows::Foundation::Uri const &u2) noexcept;
Expand All @@ -79,9 +77,9 @@ class OriginPolicyHttpFilter
winrt::Windows::Web::Http::HttpResponseMessage const &response,
bool removeAll);

OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::IHttpFilter const &innerFilter);
OriginPolicyHttpFilter(std::string &&origin, winrt::Windows::Web::Http::Filters::IHttpFilter const &innerFilter);

OriginPolicyHttpFilter();
OriginPolicyHttpFilter(std::string &&origin);

OriginPolicy ValidateRequest(winrt::Windows::Web::Http::HttpRequestMessage const &request);

Expand Down
6 changes: 2 additions & 4 deletions vnext/Shared/Networking/WinRTHttpResource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -641,8 +641,7 @@ void WinRTHttpResource::AddResponseHandler(shared_ptr<IResponseHandler> response

#pragma region IHttpResource

/*static*/ shared_ptr<IHttpResource> IHttpResource::Make(
winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept {
/*static*/ shared_ptr<IHttpResource> IHttpResource::Make(IInspectable const &inspectableProperties) noexcept {
using namespace winrt::Microsoft::ReactNative;
using winrt::Windows::Web::Http::HttpClient;

Expand All @@ -653,8 +652,7 @@ void WinRTHttpResource::AddResponseHandler(shared_ptr<IResponseHandler> response
client = HttpClient{redirFilter};
} else {
auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin");
OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin));
auto opFilter = winrt::make<OriginPolicyHttpFilter>(redirFilter);
auto opFilter = winrt::make<OriginPolicyHttpFilter>(std::move(globalOrigin), redirFilter);
redirFilter.as<RedirectHttpFilter>()->SetRedirectSource(opFilter.as<IRedirectEventSource>());

client = HttpClient{opFilter};
Expand Down