From 0f95bdc9bbf7949ca8bf436c088d5e09ea2c6e82 Mon Sep 17 00:00:00 2001
From: BlackDex <black.dex@gmail.com>
Date: Sun, 17 Jul 2022 16:21:03 +0200
Subject: [PATCH] Fix issue with CSP and icon redirects

When using anything else but the `internal` icon service it would
trigger an CSP block because the redirects were not allowed.

This PR fixes #2623 by dynamically adding the needed CSP strings.
This should also work with custom services.

For Google i needed to add an extra check because that does a redirect
it self to there gstatic.com domain.
---
 src/api/icons.rs | 24 +++---------------------
 src/config.rs    | 32 ++++++++++++++++++++++++++++++++
 src/util.rs      | 23 ++++++++++++-----------
 3 files changed, 47 insertions(+), 32 deletions(-)

diff --git a/src/api/icons.rs b/src/api/icons.rs
index 6d4d3a5a..c343df14 100644
--- a/src/api/icons.rs
+++ b/src/api/icons.rs
@@ -30,10 +30,7 @@ use crate::{
 pub fn routes() -> Vec<Route> {
     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<Redirect> {
 }
 
 #[get("/<domain>/icon.png")]
-async fn icon_custom(domain: String) -> Option<Redirect> {
-    icon_redirect(&domain, &CONFIG.icon_service()).await
-}
-
-#[get("/<domain>/icon.png")]
-async fn icon_bitwarden(domain: String) -> Option<Redirect> {
-    icon_redirect(&domain, "https://icons.bitwarden.net/{}/icon.png").await
-}
-
-#[get("/<domain>/icon.png")]
-async fn icon_duckduckgo(domain: String) -> Option<Redirect> {
-    icon_redirect(&domain, "https://icons.duckduckgo.com/ip3/{}.ico").await
-}
-
-#[get("/<domain>/icon.png")]
-async fn icon_google(domain: String) -> Option<Redirect> {
-    icon_redirect(&domain, "https://www.google.com/s2/favicons?domain={}&sz=32").await
+async fn icon_external(domain: String) -> Option<Redirect> {
+    icon_redirect(&domain, &CONFIG._icon_service_url()).await
 }
 
 #[get("/<domain>/icon.png")]
diff --git a/src/config.rs b/src/config.rs
index 8cc96f56..b8f3246b 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<bool>, smtp_explicit_tls: Option<bool>) -> String {
     if smtp_explicit_tls.is_some() || smtp_ssl.is_some() {
diff --git a/src/util.rs b/src/util.rs
index 9bf697ba..ca3667e1 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");