33// can be found in the LICENSE file.
44
55#include < algorithm>
6+ #include < cctype>
67#include < set>
78#include < sstream>
89#include < vector>
@@ -58,6 +59,7 @@ const char kFailureMsg[] = "CorsTestHandler.Failure";
5859enum class HandlerType {
5960 SERVER ,
6061 HTTP_SCHEME ,
62+ HTTP_INSECURE_SCHEME ,
6163 CUSTOM_STANDARD_SCHEME ,
6264 CUSTOM_NONSTANDARD_SCHEME ,
6365 CUSTOM_UNREGISTERED_SCHEME ,
@@ -79,6 +81,12 @@ std::string GetOrigin(HandlerType handler) {
7981 // Use HTTPS because requests from HTTP to the loopback address will be
8082 // blocked by https://chromestatus.com/feature/5436853517811712.
8183 return " https://corstest.com" ;
84+ case HandlerType::HTTP_INSECURE_SCHEME :
85+ // Like HTTP_SCHEME, but HTTP so the origin is not potentially
86+ // trustworthy. Only usable as a redirect target (served via
87+ // GetResourceHandler); as an initiator it would be subject to the
88+ // loopback address blocking described above.
89+ return " http://corstest.com" ;
8290 case HandlerType::CUSTOM_STANDARD_SCHEME :
8391 // Standard scheme that's registered as CORS and fetch enabled.
8492 // Registered in scheme_handler_unittest.cc.
@@ -102,6 +110,8 @@ std::string GetScheme(HandlerType handler) {
102110 return test_server::GetScheme (kUseHttpsServerScheme );
103111 case HandlerType::HTTP_SCHEME :
104112 return " https" ;
113+ case HandlerType::HTTP_INSECURE_SCHEME :
114+ return " http" ;
105115 case HandlerType::CUSTOM_STANDARD_SCHEME :
106116 return " customstdfetch" ;
107117 case HandlerType::CUSTOM_NONSTANDARD_SCHEME :
@@ -1725,6 +1735,180 @@ void SetupExecRedirectRequest(ExecMode mode,
17251735CORS_TEST_EXEC_REDIRECT (XhrRedirectSubresource, XHR )
17261736CORS_TEST_EXEC_REDIRECT (FetchRedirectSubresource, FETCH )
17271737
1738+ // Test that Sec-CH-* (Client Hints) and Sec-Fetch-* headers are correctly
1739+ // sanitized when a renderer-initiated subresource request is redirected.
1740+ namespace {
1741+
1742+ // Returns true if |header_map| contains a header whose name starts with
1743+ // |prefix| (case-insensitive).
1744+ bool HasHeaderWithPrefix (const CefRequest::HeaderMap& header_map,
1745+ const std::string& prefix) {
1746+ for (const auto & [name, value] : header_map) {
1747+ const std::string& name_str = name.ToString ();
1748+ if (name_str.size () < prefix.size ()) {
1749+ continue ;
1750+ }
1751+ bool match = true ;
1752+ for (size_t i = 0 ; i < prefix.size (); ++i) {
1753+ if (std::tolower (static_cast <unsigned char >(name_str[i])) !=
1754+ std::tolower (static_cast <unsigned char >(prefix[i]))) {
1755+ match = false ;
1756+ break ;
1757+ }
1758+ }
1759+ if (match) {
1760+ return true ;
1761+ }
1762+ }
1763+ return false ;
1764+ }
1765+
1766+ // Verifies the presence or absence of Sec-CH-* and Sec-Fetch-* headers on the
1767+ // received request.
1768+ struct SecHeaderResource : Resource {
1769+ bool expect_sec_ch = false ;
1770+ bool expect_sec_fetch = false ;
1771+
1772+ bool VerifyRequest (CefRefPtr<CefRequest> request) const override {
1773+ CefRequest::HeaderMap header_map;
1774+ request->GetHeaderMap (header_map);
1775+ const bool has_sec_ch = HasHeaderWithPrefix (header_map, " sec-ch-" );
1776+ const bool has_sec_fetch = HasHeaderWithPrefix (header_map, " sec-fetch-" );
1777+ EXPECT_EQ (expect_sec_ch, has_sec_ch) << GetPathURL ();
1778+ EXPECT_EQ (expect_sec_fetch, has_sec_fetch) << GetPathURL ();
1779+ return expect_sec_ch == has_sec_ch && expect_sec_fetch == has_sec_fetch;
1780+ }
1781+ };
1782+
1783+ const char kMimeTypeJs [] = " text/javascript" ;
1784+
1785+ // Returns HTML for a main page that loads |sub_url| as a classic <script>.
1786+ // The script request uses mode "no-cors", which matters for redirect path
1787+ // selection: a cross-origin redirect of a mode "cors" request whose CORS flag
1788+ // or method changes is re-issued by CorsURLLoader::StartRequest from
1789+ // CorsURLLoader's own copy of the request headers, while a "no-cors" request
1790+ // follows the redirect on the existing loader (CorsURLLoader::FollowRedirect
1791+ // -> URLLoader::FollowRedirect), which is the path that CEF replaces with
1792+ // InterceptedRequest::FollowRedirect -> Restart. Only the latter path is
1793+ // under test here. For the same reason the redirect target must be an
1794+ // http(s) URL: a redirect out of the network service's URL space (e.g. to a
1795+ // custom scheme) is re-issued by the renderer with its own copy of the
1796+ // request headers (ThrottlingURLLoader::FollowRedirectForcingRestart) and
1797+ // never takes the redirect path at all.
1798+ std::string GetScriptExecMainHtml (const std::string& sub_url) {
1799+ return " <html><head>\n "
1800+ " <script language=\" JavaScript\" >\n "
1801+ " function onResult(val) {\n "
1802+ " if (val === '" +
1803+ std::string (kDefaultText ) + " ') {" + GetSuccessMsgJS () + " } else {" +
1804+ GetFailureMsgJS () +
1805+ " }\n }\n "
1806+ " function execRequest() {\n "
1807+ " var s = document.createElement('script');\n "
1808+ " s.src = '" +
1809+ sub_url +
1810+ " ';\n "
1811+ " s.onerror = function() { onResult('FAILURE'); };\n "
1812+ " document.head.appendChild(s);\n "
1813+ " }\n </script>\n "
1814+ " </head><body onload=\" execRequest();\" >"
1815+ " Running execRequest..."
1816+ " </body></html>" ;
1817+ }
1818+
1819+ // On redirect CEF restarts the request through the URLLoaderFactory (see
1820+ // InterceptedRequest::FollowRedirect -> Restart) with the header set from the
1821+ // previous hop. Sec-Fetch-* headers are always removed before the restart
1822+ // (see XhrRedirectSubresource above) and re-derived by the network service
1823+ // for requests that reach it. Sec-CH-* headers must be removed only when the
1824+ // redirect goes from a potentially trustworthy URL (here, the loopback
1825+ // server) to an untrustworthy URL (here, an http URL on a non-loopback
1826+ // host), matching network::MaybeRemoveSecHeaders behavior on Chromium's
1827+ // in-place redirect path; on all other redirects they must be preserved
1828+ // because nothing re-derives them.
1829+ //
1830+ // The initial subresource request is same-origin to the (potentially
1831+ // trustworthy) server, so it carries both the renderer-set Sec-CH-* headers
1832+ // and the network-managed Sec-Fetch-* headers; that's asserted on the
1833+ // redirecting resource so the target expectations can't pass vacuously.
1834+ void SetupScriptRedirectSecHeadersRequest (HandlerType target_handler,
1835+ bool expect_target_sec_ch,
1836+ bool expect_target_sec_fetch,
1837+ TestSetup* setup,
1838+ const std::string& test_name,
1839+ Resource* main_resource,
1840+ SecHeaderResource* redirect_resource,
1841+ SecHeaderResource* target_resource) {
1842+ const std::string& base_path = " /" + test_name;
1843+
1844+ // Final resource the script request lands on after the redirect. Executing
1845+ // it reports success back to the main page.
1846+ target_resource->Init (target_handler, base_path + " .target.js" , kMimeTypeJs ,
1847+ " onResult('" + std::string (kDefaultText ) + " ');" );
1848+ target_resource->expect_sec_ch = expect_target_sec_ch;
1849+ target_resource->expect_sec_fetch = expect_target_sec_fetch;
1850+
1851+ // Script request returns a redirect to the final resource.
1852+ redirect_resource->Init (HandlerType::SERVER , base_path + " .sub.js" ,
1853+ kMimeTypeJs , std::string ());
1854+ redirect_resource->expect_sec_ch = true ;
1855+ redirect_resource->expect_sec_fetch = true ;
1856+ SetupRedirectResponse (RedirectMode::MODE_302 , target_resource->GetPathURL (),
1857+ redirect_resource->response );
1858+
1859+ // Main page issues the script request to the redirecting URL.
1860+ main_resource->Init (HandlerType::SERVER , base_path, kMimeTypeHtml ,
1861+ GetScriptExecMainHtml (redirect_resource->GetPathURL ()));
1862+ main_resource->expected_success_query_ct = 1 ;
1863+
1864+ setup->AddResource (main_resource);
1865+ setup->AddResource (redirect_resource);
1866+ setup->AddResource (target_resource);
1867+ }
1868+
1869+ } // namespace
1870+
1871+ #define CORS_TEST_SCRIPT_REDIRECT_SEC_HEADERS (test_name, target_handler, \
1872+ expect_sec_ch, expect_sec_fetch) \
1873+ TEST (CorsTest, test_name) { \
1874+ TestSetup setup; \
1875+ Resource resource_main; \
1876+ SecHeaderResource resource_redirect; \
1877+ SecHeaderResource resource_target; \
1878+ SetupScriptRedirectSecHeadersRequest ( \
1879+ HandlerType::target_handler, expect_sec_ch, expect_sec_fetch, &setup, \
1880+ " CorsTest." #test_name, &resource_main, &resource_redirect, \
1881+ &resource_target); \
1882+ CefRefPtr<CorsTestHandler> handler = new CorsTestHandler (&setup); \
1883+ handler->ExecuteTest (); \
1884+ ReleaseAndWaitForDestructor (handler); \
1885+ }
1886+
1887+ // Same-trustworthiness redirect (server to server): Sec-CH-* headers are
1888+ // preserved and Sec-Fetch-* headers are re-derived by the network service.
1889+ CORS_TEST_SCRIPT_REDIRECT_SEC_HEADERS (ScriptRedirectSubresourceSecHeadersServer,
1890+ SERVER ,
1891+ /* expect_target_sec_ch=*/ true ,
1892+ /* expect_target_sec_fetch=*/ true )
1893+
1894+ // Cross-origin redirect to a potentially trustworthy https URL (served via
1895+ // GetResourceHandler): Sec-CH-* headers are preserved. Sec-Fetch-* headers
1896+ // are removed before the restart and nothing re-derives them for requests
1897+ // that don't reach the network service.
1898+ CORS_TEST_SCRIPT_REDIRECT_SEC_HEADERS (
1899+ ScriptRedirectSubresourceSecHeadersHttpScheme,
1900+ HTTP_SCHEME ,
1901+ /* expect_target_sec_ch=*/ true ,
1902+ /* expect_target_sec_fetch=*/ false )
1903+
1904+ // Cross-origin redirect that downgrades to an untrustworthy http URL (served
1905+ // via GetResourceHandler): both header families are removed.
1906+ CORS_TEST_SCRIPT_REDIRECT_SEC_HEADERS (
1907+ ScriptRedirectSubresourceSecHeadersInsecureHttpScheme,
1908+ HTTP_INSECURE_SCHEME ,
1909+ /* expect_target_sec_ch=*/ false ,
1910+ /* expect_target_sec_fetch=*/ false )
1911+
17281912namespace {
17291913
17301914struct PostResource : CookieResource {
0 commit comments