diff --git a/change/react-native-windows-bc10ca83-c1a9-4f4a-ac53-6d54378c3aca.json b/change/react-native-windows-bc10ca83-c1a9-4f4a-ac53-6d54378c3aca.json new file mode 100644 index 00000000000..f36a7791478 --- /dev/null +++ b/change/react-native-windows-bc10ca83-c1a9-4f4a-ac53-6d54378c3aca.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Make HTTP origin value an instance member", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp b/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp index c15b0bf0f69..dc0f7ee2964 100644 --- a/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp +++ b/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp @@ -280,10 +280,9 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { // Should implicitly set Content-Length and Content-Type request.Content(HttpStringContent{L"PreflightContent"}); - auto filter = winrt::make(mockFilter); + auto filter = winrt::make("http://somehost", mockFilter); auto opFilter = filter.as(); - OriginPolicyHttpFilter::SetStaticOrigin("http://somehost"); try { auto sendOp = opFilter->SendPreflightAsync(request); sendOp.get(); @@ -291,12 +290,10 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { 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()); } } @@ -323,10 +320,9 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { // Should implicitly set Content-Length and Content-Type request.Content(HttpStringContent{L"PreflightContent"}); - auto filter = winrt::make(mockFilter); + auto filter = winrt::make("http://somehost", mockFilter); auto opFilter = filter.as(); - OriginPolicyHttpFilter::SetStaticOrigin("http://somehost"); try { auto sendOp = opFilter->SendPreflightAsync(request); sendOp.get(); @@ -334,12 +330,10 @@ TEST_CLASS (OriginPolicyHttpFilterTest) { 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()); } } diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp index 744f4d2464e..1179be69311 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp @@ -18,6 +18,7 @@ #include using std::set; +using std::string; using std::wstring; using winrt::hresult_error; @@ -114,15 +115,6 @@ bool OriginPolicyHttpFilter::CaseInsensitiveComparer::operator()(const wstring & /*static*/ set 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(); } @@ -387,10 +379,14 @@ bool OriginPolicyHttpFilter::CaseInsensitiveComparer::operator()(const wstring & } } -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 = @@ -400,17 +396,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)) @@ -426,7 +422,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())) @@ -435,7 +431,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; @@ -472,7 +468,7 @@ void OriginPolicyHttpFilter::ValidateAllowOrigin( // 4.10.4 - Mismatched allow origin auto taintedOriginProp = props.TryLookup(L"TaintedOrigin"); auto taintedOrigin = taintedOriginProp && winrt::unbox_value(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()) @@ -603,7 +599,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()}; } } @@ -691,7 +687,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)}; @@ -708,7 +704,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. @@ -740,7 +736,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()->WithCredentials = false; @@ -777,7 +773,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); diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.h b/vnext/Shared/Networking/OriginPolicyHttpFilter.h index f6a86cbae29..ba8738d5636 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.h +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.h @@ -37,9 +37,6 @@ class OriginPolicyHttpFilter static std::set s_corsForbiddenRequestHeaderNamePrefixes; static std::set 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; @@ -49,11 +46,11 @@ class OriginPolicyHttpFilter 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; @@ -80,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); diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index 899fb157b86..7c81783b2e7 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -641,8 +641,7 @@ void WinRTHttpResource::AddResponseHandler(shared_ptr response #pragma region IHttpResource -/*static*/ shared_ptr IHttpResource::Make( - winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept { +/*static*/ shared_ptr IHttpResource::Make(IInspectable const &inspectableProperties) noexcept { using namespace winrt::Microsoft::ReactNative; using winrt::Windows::Web::Http::HttpClient; @@ -653,8 +652,7 @@ void WinRTHttpResource::AddResponseHandler(shared_ptr response client = HttpClient{redirFilter}; } else { auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); - OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin)); - auto opFilter = winrt::make(redirFilter); + auto opFilter = winrt::make(std::move(globalOrigin), redirFilter); redirFilter.as()->SetRedirectSource(opFilter.as()); client = HttpClient{opFilter};