diff --git a/src/api/icons.rs b/src/api/icons.rs index 6d4d3a5a75..c343df142a 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -30,10 +30,7 @@ use crate::{ pub fn routes() -> Vec { match CONFIG.icon_service().as_str() { "internal" => routes![icon_internal], - "bitwarden" => routes![icon_bitwarden], - "duckduckgo" => routes![icon_duckduckgo], - "google" => routes![icon_google], - _ => routes![icon_custom], + _ => routes![icon_external], } } @@ -100,23 +97,8 @@ async fn icon_redirect(domain: &str, template: &str) -> Option { } #[get("//icon.png")] -async fn icon_custom(domain: String) -> Option { - icon_redirect(&domain, &CONFIG.icon_service()).await -} - -#[get("//icon.png")] -async fn icon_bitwarden(domain: String) -> Option { - icon_redirect(&domain, "https://icons.bitwarden.net/{}/icon.png").await -} - -#[get("//icon.png")] -async fn icon_duckduckgo(domain: String) -> Option { - icon_redirect(&domain, "https://icons.duckduckgo.com/ip3/{}.ico").await -} - -#[get("//icon.png")] -async fn icon_google(domain: String) -> Option { - icon_redirect(&domain, "https://www.google.com/s2/favicons?domain={}&sz=32").await +async fn icon_external(domain: String) -> Option { + icon_redirect(&domain, &CONFIG._icon_service_url()).await } #[get("//icon.png")] diff --git a/src/config.rs b/src/config.rs index 8cc96f568a..b8f3246ba3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -463,6 +463,10 @@ make_config! { /// service is set, an icon request to Vaultwarden will return an HTTP redirect to the /// corresponding icon at the external service. icon_service: String, false, def, "internal".to_string(); + /// Internal + _icon_service_url: String, false, gen, |c| generate_icon_service_url(&c.icon_service); + /// Internal + _icon_service_csp: String, false, gen, |c| generate_icon_service_csp(&c.icon_service, &c._icon_service_url); /// Icon redirect code |> The HTTP status code to use for redirects to an external icon service. /// The supported codes are 301 (legacy permanent), 302 (legacy temporary), 307 (temporary), and 308 (permanent). /// Temporary redirects are useful while testing different icon services, but once a service @@ -748,6 +752,34 @@ fn extract_url_path(url: &str) -> String { } } +/// Generate the correct URL for the icon service. +/// This will be used within icons.rs to call the external icon service. +fn generate_icon_service_url(icon_service: &str) -> String { + match icon_service { + "internal" => "".to_string(), + "bitwarden" => "https://icons.bitwarden.net/{}/icon.png".to_string(), + "duckduckgo" => "https://icons.duckduckgo.com/ip3/{}.ico".to_string(), + "google" => "https://www.google.com/s2/favicons?domain={}&sz=32".to_string(), + _ => icon_service.to_string(), + } +} + +/// Generate the CSP string needed to allow redirected icon fetching +fn generate_icon_service_csp(icon_service: &str, icon_service_url: &str) -> String { + // We split on the first '{', since that is the variable delimiter for an icon service URL. + // Everything up until the first '{' should be fixed and can be used as an CSP string. + let csp_string = match icon_service_url.split_once('{') { + Some((c, _)) => c.to_string(), + None => "".to_string(), + }; + + // Because Google does a second redirect to there gstatic.com domain, we need to add an extra csp string. + match icon_service { + "google" => csp_string + " https://*.gstatic.com/favicon", + _ => csp_string, + } +} + /// Convert the old SMTP_SSL and SMTP_EXPLICIT_TLS options fn smtp_convert_deprecated_ssl_options(smtp_ssl: Option, smtp_explicit_tls: Option) -> String { if smtp_explicit_tls.is_some() || smtp_ssl.is_some() { diff --git a/src/util.rs b/src/util.rs index 9bf697bafe..ca3667e1b0 100644 --- a/src/util.rs +++ b/src/util.rs @@ -38,18 +38,18 @@ impl Fairing for AppHeaders { let req_uri_path = req.uri().path(); - // Check if we are requesting an admin page, if so, allow unsafe-inline for scripts. - // TODO: In the future maybe we need to see if we can generate a sha256 hash or have no scripts inline at all. - let admin_path = format!("{}/admin", CONFIG.domain_path()); - let mut script_src = ""; - if req_uri_path.starts_with(admin_path.as_str()) { - script_src = " 'unsafe-inline'"; - } - // Do not send the Content-Security-Policy (CSP) Header and X-Frame-Options for the *-connector.html files. // This can cause issues when some MFA requests needs to open a popup or page within the clients like WebAuthn, or Duo. // This is the same behaviour as upstream Bitwarden. if !req_uri_path.ends_with("connector.html") { + // Check if we are requesting an admin page, if so, allow unsafe-inline for scripts. + // TODO: In the future maybe we need to see if we can generate a sha256 hash or have no scripts inline at all. + let admin_path = format!("{}/admin", CONFIG.domain_path()); + let mut script_src = ""; + if req_uri_path.starts_with(admin_path.as_str()) { + script_src = " 'unsafe-inline'"; + } + // # Frame Ancestors: // Chrome Web Store: https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb // Edge Add-ons: https://microsoftedge.microsoft.com/addons/detail/bitwarden-free-password/jbkfoedolllekgbhcbcoahefnbanhhlh?hl=en-US @@ -65,13 +65,14 @@ impl Fairing for AppHeaders { "default-src 'self'; \ script-src 'self'{script_src}; \ style-src 'self' 'unsafe-inline'; \ - img-src 'self' data: https://haveibeenpwned.com/ https://www.gravatar.com; \ + img-src 'self' data: https://haveibeenpwned.com/ https://www.gravatar.com {icon_service_csp}; \ child-src 'self' https://*.duosecurity.com https://*.duofederal.com; \ frame-src 'self' https://*.duosecurity.com https://*.duofederal.com; \ connect-src 'self' https://api.pwnedpasswords.com/range/ https://2fa.directory/api/ https://app.simplelogin.io/api/ https://app.anonaddy.com/api/ https://relay.firefox.com/api/; \ object-src 'self' blob:; \ - frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh moz-extension://* {};", - CONFIG.allowed_iframe_ancestors() + frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh moz-extension://* {allowed_iframe_ancestors};", + icon_service_csp=CONFIG._icon_service_csp(), + allowed_iframe_ancestors=CONFIG.allowed_iframe_ancestors() ); res.set_raw_header("Content-Security-Policy", csp); res.set_raw_header("X-Frame-Options", "SAMEORIGIN");