From 1d0eaac260d251abed23106e6356cb07e5b6e994 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Sat, 3 Apr 2021 22:51:44 +0200 Subject: [PATCH] Updated icon fetching. - Added image type checking, and prevent downloading non images. We didn't checked this before, which could in turn could allow someone to download an arbitrary file. - This also prevents SVG images from being used, while they work on the web-vault and desktop client, they didn't on the mobile versions. - Because of this image type checking we can return a valid file type instead of only 'x-icon' (which is still used as a fallback). - Prevent rel values with `icon-mask`, these are not valid favicons. --- src/api/icons.rs | 58 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/api/icons.rs b/src/api/icons.rs index 6da3af0b..3f8a41e1 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -37,6 +37,7 @@ static CLIENT: Lazy = Lazy::new(|| { // Build Regex only once since this takes a lot of time. static ICON_REL_REGEX: Lazy = Lazy::new(|| Regex::new(r"(?i)icon$|apple.*icon").unwrap()); +static ICON_REL_BLACKLIST: Lazy = Lazy::new(|| Regex::new(r"(?i)mask-icon").unwrap()); static ICON_SIZE_REGEX: Lazy = Lazy::new(|| Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap()); // Special HashMap which holds the user defined Regex to speedup matching the regex. @@ -52,7 +53,9 @@ fn icon(domain: String) -> Cached>> { } match get_icon(&domain) { - Some(i) => Cached::ttl(Content(ContentType::new("image", "x-icon"), i), CONFIG.icon_cache_ttl()), + Some((icon, icon_type)) => { + Cached::ttl(Content(ContentType::new("image", icon_type), icon), CONFIG.icon_cache_ttl()) + }, _ => Cached::ttl(Content(ContentType::new("image", "png"), FALLBACK_ICON.to_vec()), CONFIG.icon_cache_negttl()), } } @@ -243,7 +246,7 @@ fn is_domain_blacklisted(domain: &str) -> bool { is_blacklisted } -fn get_icon(domain: &str) -> Option> { +fn get_icon(domain: &str) -> Option<(Vec, String)> { let path = format!("{}/{}.png", CONFIG.icon_cache_folder(), domain); // Check for expiration of negatively cached copy @@ -252,7 +255,11 @@ fn get_icon(domain: &str) -> Option> { } if let Some(icon) = get_cached_icon(&path) { - return Some(icon); + let icon_type = match get_icon_type(&icon) { + Some(x) => x, + _ => "x-icon", + }; + return Some((icon, icon_type.to_string())); } if CONFIG.disable_icon_download() { @@ -261,9 +268,9 @@ fn get_icon(domain: &str) -> Option> { // Get the icon, or None in case of error match download_icon(&domain) { - Ok(icon) => { + Ok((icon, icon_type)) => { save_icon(&path, &icon); - Some(icon) + Some((icon, icon_type.unwrap_or("x-icon").to_string())) } Err(e) => { error!("Error downloading icon: {:?}", e); @@ -324,7 +331,6 @@ fn icon_is_expired(path: &str) -> bool { expired.unwrap_or(true) } -#[derive(Debug)] struct Icon { priority: u8, href: String, @@ -348,7 +354,7 @@ fn get_favicons_node(node: &std::rc::Rc, icons: &mut Ve let attr_name = attr.name.local.as_ref(); let attr_value = attr.value.as_ref(); - if attr_name == "rel" && ICON_REL_REGEX.is_match(attr_value) { + if attr_name == "rel" && ICON_REL_REGEX.is_match(attr_value) && !ICON_REL_BLACKLIST.is_match(attr_value) { has_rel = true; } else if attr_name == "href" { href = Some(attr_value); @@ -597,7 +603,7 @@ fn parse_sizes(sizes: Option<&str>) -> (u16, u16) { (width, height) } -fn download_icon(domain: &str) -> Result, Error> { +fn download_icon(domain: &str) -> Result<(Vec, Option<&str>), Error> { if is_domain_blacklisted(domain) { err!("Domain is blacklisted", domain) } @@ -605,6 +611,7 @@ fn download_icon(domain: &str) -> Result, Error> { let icon_result = get_icon_url(&domain)?; let mut buffer = Vec::new(); + let mut icon_type: Option<&str> = None; use data_url::DataUrl; @@ -616,17 +623,31 @@ fn download_icon(domain: &str) -> Result, Error> { Ok((body, _fragment)) => { // Also check if the size is atleast 67 bytes, which seems to be the smallest png i could create if body.len() >= 67 { + // Check if the icon type is allowed, else try an icon from the list. + icon_type = get_icon_type(&body); + if icon_type.is_none() { + debug!("Icon from {} data:image uri, is not a valid image type", domain); + continue; + } + info!("Extracted icon from data:image uri for {}", domain); buffer = body; break; } } - _ => warn!("data uri is invalid"), + _ => warn!("Extracted icon from data:image uri is invalid"), }; } else { match get_page_with_cookies(&icon.href, &icon_result.cookies, &icon_result.referer) { Ok(mut res) => { - info!("Downloaded icon from {}", icon.href); res.copy_to(&mut buffer)?; + // Check if the icon type is allowed, else try an icon from the list. + icon_type = get_icon_type(&buffer); + if icon_type.is_none() { + buffer.clear(); + debug!("Icon from {}, is not a valid image type", icon.href); + continue; + } + info!("Downloaded icon from {}", icon.href); break; } _ => warn!("Download failed for {}", icon.href), @@ -635,10 +656,10 @@ fn download_icon(domain: &str) -> Result, Error> { } if buffer.is_empty() { - err!("Empty response") + err!("Empty response downloading icon") } - Ok(buffer) + Ok((buffer, icon_type)) } fn save_icon(path: &str, icon: &[u8]) { @@ -650,7 +671,18 @@ fn save_icon(path: &str, icon: &[u8]) { create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache"); } Err(e) => { - info!("Icon save error: {:?}", e); + warn!("Icon save error: {:?}", e); } } } + +fn get_icon_type(bytes: &[u8]) -> Option<&'static str> { + match bytes { + [137, 80, 78, 71, ..] => Some("png"), + [0, 0, 1, 0, ..] => Some("x-icon"), + [82, 73, 70, 70, ..] => Some("webp"), + [255, 216, 255, ..] => Some("jpeg"), + [66, 77, ..] => Some("bmp"), + _ => None + } +}