|
| 1 | +Additional Allowed Frame Ancestors for iframes |
| 2 | +=== |
| 3 | + |
| 4 | +# Background |
| 5 | +Due to potential [Clickjacking](https://en.wikipedia.org/wiki/Clickjacking) attack, a lot of sites only allow themselves to be hosted in certain trusted ancestor iframes and pages. The main way to specify this ancestor requirement for sites are http header [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) and [Content-Security-Policy frame-ancestors directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors). |
| 6 | + |
| 7 | +However, there are application scenarios that require hosting these sites in the app's UI that is authored as an HTML page. |
| 8 | +`<webview>` HTML element was provided for these hosting scenarios in previous solutions like Electron and JavaScript UWP apps. |
| 9 | + |
| 10 | +For WebView2, we are providing a native API for these hosting scenarios. Developers can use it to provide additional allowed frame ancestors as if the site sent these as part of the Content-Security-Policy frame-ancestors directive. The result is that an ancestor is allowed if it is allowed by the site's original policies or by this additional allowed frame ancestors. |
| 11 | + |
| 12 | +# Conceptual pages (How To) |
| 13 | + |
| 14 | +To host other sites in an trusted page with modified allowed frame ancestors |
| 15 | +- Listen to FrameNavigationStarting event of CoreWebView2. |
| 16 | +- Set AdditionalAllowedFrameAncestors property of the NavigationStartingEventArgs to a list additional allowed frame ancestors using the same syntax as [Content-Security-Policy frame-ancestors directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors). |
| 17 | + |
| 18 | +The list should normally only contain the origin of the top page. |
| 19 | +If you are hosting other sites through nested iframes and the origins of some of the intermediate iframes are different from the origin of the top page and those origins might not be allowed by the site's original policies, the list should also include those origins. |
| 20 | + |
| 21 | +You should only add an origin to the list if it is fully trusted. You should limit the usage of the API to the targetted app scenarios. |
| 22 | + |
| 23 | +# Examples |
| 24 | +## Win32 C++ |
| 25 | +```cpp |
| 26 | +const std::wstring myTrustedSite = L"https://example.com/"; |
| 27 | +const std::wstring siteToHost = L"https://www.microsoft.com/"; |
| 28 | + |
| 29 | +bool AreSitesSame(PCWSTR url1, PCWSTR url2) |
| 30 | +{ |
| 31 | + wil::com_ptr<IUri> uri1; |
| 32 | + CHECK_FAILURE(CreateUri(url1.c_str(), Uri_CREATE_CANONICALIZE, 0, &uri1)); |
| 33 | + DWORD scheme1 = URL_SCHEME_INVALID; |
| 34 | + DWORD port1 = 0; |
| 35 | + wil::unique_bstr host1; |
| 36 | + CHECK_FAILURE(uri1->GetScheme(&scheme1)); |
| 37 | + CHECK_FAILURE(uri1->GetHost(&host1)); |
| 38 | + CHECK_FAILURE(uri1->GetPort(&port1)); |
| 39 | + wil::com_ptr<IUri> uri2; |
| 40 | + CHECK_FAILURE(CreateUri(url2.c_str(), Uri_CREATE_CANONICALIZE, 0, &uri2)); |
| 41 | + DWORD scheme2 = URL_SCHEME_INVALID; |
| 42 | + DWORD port2 = 0; |
| 43 | + wil::unique_bstr host2; |
| 44 | + CHECK_FAILURE(uri2->GetScheme(&scheme2)); |
| 45 | + CHECK_FAILURE(uri2->GetHost(&host2)); |
| 46 | + CHECK_FAILURE(uri2->GetPort(&port2)); |
| 47 | + return (scheme1 == scheme2) && (port1 == port2) && (wcscmp(host1.get(), host2.get()) == 0); |
| 48 | +} |
| 49 | + |
| 50 | +// App specific logic to decide whether the page is fully trusted. |
| 51 | +bool IsAppContentUri(PCWSTR pageUrl) |
| 52 | +{ |
| 53 | + return AreSitesSame(pageUrl, myTrustedSite); |
| 54 | +} |
| 55 | + |
| 56 | +// App specific logic to decide whether a site is the one it wants to host. |
| 57 | +bool IsTargetSite(PCWSTR siteUrl) |
| 58 | +{ |
| 59 | + return AreSitesSame(siteUrl, siteToHost); |
| 60 | +} |
| 61 | + |
| 62 | +void MyApp::HandleHostedSites() |
| 63 | +{ |
| 64 | + CHECK_FAILURE(m_webview->add_FrameCreated( |
| 65 | + Callback<ICoreWebView2FrameCreatedEventHandler>( |
| 66 | + [this](ICoreWebView2* sender, ICoreWebView2FrameCreatedEventArgs* args) |
| 67 | + -> HRESULT |
| 68 | + { |
| 69 | + wil::unique_cotaskmem_string pageUrl; |
| 70 | + CHECK_FAILURE(m_webView->get_Source(&pageUrl)); |
| 71 | + // IsAppContentUri verifies that pageUrl is app's content. |
| 72 | + if (IsAppContentUri(pageUrl.get())) |
| 73 | + { |
| 74 | + // We are on trusted pages. Now check whether it is the iframe we plan |
| 75 | + // to host other sites. |
| 76 | + const std::wstring siteHostingFrameName = L"my_site_hosting_frame"; |
| 77 | + wil::com_ptr<ICoreWebView2Frame> webviewFrame; |
| 78 | + CHECK_FAILURE(args->get_Frame(&webviewFrame)); |
| 79 | + wil::unique_cotaskmem_string frameName; |
| 80 | + CHECK_FAILURE(webviewFrame->get_Name(&frameName)); |
| 81 | + if (siteHostingFrameName == frameName.get()) |
| 82 | + { |
| 83 | + // We are hosting sites. |
| 84 | + m_hostingSite = true; |
| 85 | + CHECK_FAILURE(webviewFrame->add_Destroyed( |
| 86 | + Microsoft::WRL::Callback< |
| 87 | + ICoreWebView2FrameDestroyedEventHandler>( |
| 88 | + [this](ICoreWebView2Frame* sender, |
| 89 | + IUnknown* args) -> HRESULT { |
| 90 | + m_hostingSite = false; |
| 91 | + return S_OK; |
| 92 | + }) |
| 93 | + .Get(), |
| 94 | + nullptr)); |
| 95 | + } |
| 96 | + } |
| 97 | + return S_OK; |
| 98 | + }) |
| 99 | + .Get(), |
| 100 | + nullptr)); |
| 101 | + // Using FrameNavigationStarting event instead of NavigationStarting event of CoreWebViewFrame |
| 102 | + // to cover all possible nested iframes inside the hosted site as CoreWebViewFrame |
| 103 | + // object currently only support first level iframes in the top page. |
| 104 | + CHECK_FAILURE(m_webview->add_FrameNavigationStarting( |
| 105 | + Microsoft::WRL::Callback<ICoreWebView2NavigationStartingEventHandler>( |
| 106 | + [this]( |
| 107 | + ICoreWebView2* sender, |
| 108 | + ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT |
| 109 | + { |
| 110 | + if (m_hostingSite) |
| 111 | + { |
| 112 | + wil::unique_cotaskmem_string navigationTargetUri; |
| 113 | + CHECK_FAILURE(args->get_Uri(&navigationTargetUri)); |
| 114 | + wil::com_ptr< |
| 115 | + ICoreWebView2NavigationStartingEventArgs_2> |
| 116 | + navigationStartArgs; |
| 117 | + if (SUCCEEDED(args->QueryInterface( |
| 118 | + IID_PPV_ARGS(&navigationStartArgs))) && |
| 119 | + IsTargetSite(navigationTargetUri.get())) |
| 120 | + { |
| 121 | + navigationStartArgs |
| 122 | + ->put_AdditionalAllowedFrameAncestors( |
| 123 | + myTrustedSite); |
| 124 | + } |
| 125 | + } |
| 126 | + return S_OK; |
| 127 | + }) |
| 128 | + .Get(), |
| 129 | + nullptr)); |
| 130 | +} |
| 131 | +``` |
| 132 | +## WinRT and .NET |
| 133 | +```c# |
| 134 | + const string myTrustedSite = "https://example.com/"; |
| 135 | + const string siteToHost = "https://www.microsoft.com"; |
| 136 | + private bool AreSitesSame(string url1, string url2) |
| 137 | + { |
| 138 | + auto uri1 = new Uri(url1); |
| 139 | + auto uri2 = new Uri(url2); |
| 140 | + return (uri1.SchemeName == uri2.SchemeName) && (uri1.Host == uri2.Host) && (uri1.Port == uri2.Port); |
| 141 | + } |
| 142 | + private bool IsAppContentUri(string pageUrl) |
| 143 | + { |
| 144 | + // App specific logic to decide whether the page is fully trusted. |
| 145 | + return AreSitesSame(pageUrl, myTrustedSite); |
| 146 | + } |
| 147 | +
|
| 148 | + private bool IsTargetSite(string siteUrl) |
| 149 | + { |
| 150 | + // App specific logic to decide whether the site is the one it wants to host. |
| 151 | + return AreSitesSame(siteUrl, siteToHost); |
| 152 | + } |
| 153 | +
|
| 154 | + private void CoreWebView2_FrameCreated(CoreWebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2FrameCreatedEventArgs args) |
| 155 | + { |
| 156 | + // my_site_hosting_frame is the name attribute on the iframe element that we used in the web page to host the site. |
| 157 | + const string siteHostingFrameName = "my_site_hosting_frame"; |
| 158 | + if (IsAppContentUri(sender.Source) && (args.Frame.Name == siteHostingFrameName)) |
| 159 | + { |
| 160 | + m_hostingSite = true; |
| 161 | + args.Frame.Destroyed += CoreWebView2_SiteHostingFrameDestroyed; |
| 162 | + } |
| 163 | + } |
| 164 | +
|
| 165 | + private void CoreWebView2_SiteHostingFrameDestroyed(CoreWebView2Frame sender, Object args) |
| 166 | + { |
| 167 | + m_hostingSite = false; |
| 168 | + } |
| 169 | +
|
| 170 | + // Using FrameNavigationStarting event instead of NavigationStarting event of CoreWebViewFrame |
| 171 | + // to cover all possible nested iframes inside the hosted site as CoreWebViewFrame |
| 172 | + // object currently only support first level iframes in the top page. |
| 173 | + private void CoreWebView2_FrameNavigationStarting(CoreWebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs args) |
| 174 | + { |
| 175 | + if (IsTargetSite(args.Uri)) |
| 176 | + { |
| 177 | + args.AdditionalAllowedFrameAncestors = myTrustedSite; |
| 178 | + } |
| 179 | + } |
| 180 | + private void HandleHostedSites() |
| 181 | + { |
| 182 | + webView.FrameCreated += CoreWebView2_FrameCreated; |
| 183 | + webView.FrameNavigationStarting += CoreWebView2_FrameNavigationStarting; |
| 184 | + } |
| 185 | +``` |
| 186 | + |
| 187 | +# API Details |
| 188 | +## Win32 C++ |
| 189 | +``` |
| 190 | +interface ICoreWebView2NavigationStartingEventArgs_2 : ICoreWebView2NavigationStartingEventArgs |
| 191 | +{ |
| 192 | +
|
| 193 | + /// Get additional allowed frame ancestors set by the host app. |
| 194 | + [propget] HRESULT AdditionalAllowedFrameAncestors([out, retval] LPWSTR* value); |
| 195 | +
|
| 196 | + /// The host may set this property to allow a frame to be hosted by certain additional ancestors besides what is allowed by |
| 197 | + /// http header [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) |
| 198 | + /// and [Content-Security-Policy frame-ancestors directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors). |
| 199 | + /// If set, a frame ancestor is allowed if it is allowed by the additional allowed frame ancestoers or original http header from the site. |
| 200 | + /// Whether an ancestor is allowed by the additional allowed frame ancestoers is done the same way as if the site provided |
| 201 | + /// it as the source list of the Content-Security-Policy frame-ancestors directive. |
| 202 | + /// This property gives the app the ability to use iframe to host sites that otherwise |
| 203 | + /// could not be hosted in an iframe in trusted app pages. |
| 204 | + /// This could potentially subject the hosted sites to [Clickjacking](https://en.wikipedia.org/wiki/Clickjacking) |
| 205 | + /// attack from the code running in the hosting web page. Therefore, you should only |
| 206 | + /// set this property with origins of fully trusted hosting page and any intermediate iframes. |
| 207 | + /// Whenever possible, you should use the list of specific origins of the top and intermediate |
| 208 | + /// frames instead of wildcard characters for this property. |
| 209 | + /// This API is to provide limited support for app scenarios that used to be supported by |
| 210 | + /// `<webview>` element in other solutions like JavaScript UWP apps and Electron. |
| 211 | + /// You should limit the usage of this property to trusted pages, and if possible, to specific iframe and |
| 212 | + /// specific navigation target url, by checking the `Source` of the WebView2, the `Name` |
| 213 | + /// of the ICoreWebView2Frame and `Uri` of the event args. |
| 214 | + /// This property is ignored for top level document navigation. |
| 215 | + /// |
| 216 | + [propput] HRESULT AdditionalAllowedFrameAncestors([in] LPCWSTR value); |
| 217 | +
|
| 218 | +} |
| 219 | +``` |
| 220 | +## WinRT and .NET |
| 221 | +```c# |
| 222 | +namespace Microsoft.Web.WebView2.Core |
| 223 | +{ |
| 224 | + runtimeclass CoreWebView2NavigationStartingEventArgs |
| 225 | + { |
| 226 | + // ... |
| 227 | +
|
| 228 | + [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2NavigationStartingEventArgs_2")] |
| 229 | + { |
| 230 | + String AdditionalAllowedFrameAncestors { get; set; }; |
| 231 | + } |
| 232 | + } |
| 233 | +} |
| 234 | +``` |
0 commit comments