[librsvg] allowed_url.rs: New file; logic to decide whether to allow loading files



commit 7534fd46a1e295fbc6ff9cfa199d29152b8542bf
Author: Federico Mena Quintero <federico gnome org>
Date:   Mon Nov 26 12:05:02 2018 -0600

    allowed_url.rs: New file; logic to decide whether to allow loading files

 rsvg_internals/src/allowed_url.rs | 221 ++++++++++++++++++++++++++++++++++++++
 rsvg_internals/src/lib.rs         |   1 +
 2 files changed, 222 insertions(+)
---
diff --git a/rsvg_internals/src/allowed_url.rs b/rsvg_internals/src/allowed_url.rs
new file mode 100644
index 00000000..bc997c3f
--- /dev/null
+++ b/rsvg_internals/src/allowed_url.rs
@@ -0,0 +1,221 @@
+use std::io;
+use std::path::{Path, PathBuf};
+use url::{self, Url};
+
+/// Wrapper for URLs which are allowed to be loaded
+///
+/// SVG files can reference other files (PNG/JPEG images, other SVGs,
+/// CSS files, etc.).  This object is constructed by checking whether
+/// a specified `href` (a possibly-relative filename, for example)
+/// should be allowed to be loaded, given the base URL of the SVG
+/// being loaded.
+#[derive(Debug, PartialEq)]
+pub struct AllowedUrl(Url);
+
+#[derive(Debug, PartialEq)]
+pub enum AllowedUrlError {
+    /// parsing error from `Url::parse()`
+    HrefParseError(url::ParseError),
+
+    /// A base file/uri was not set
+    BaseRequired,
+
+    /// Cannot reference a file with a different URI scheme from the base file
+    DifferentURISchemes,
+
+    /// Some scheme we don't allow loading
+    DisallowedScheme,
+
+    /// The requested file is not in the same directory as the base file,
+    /// or in one directory below the base file.
+    NotSiblingOrChildOfBaseFile,
+
+    /// Error when obtaining the file path or the base file path
+    InvalidPath,
+
+    /// The base file cannot be the root of the file system
+    BaseIsRoot,
+
+    /// Error when canonicalizing either the file path or the base file path
+    CanonicalizationError,
+}
+
+impl AllowedUrl {
+    pub fn from_href(href: &str, base_url: Option<&Url>) -> Result<AllowedUrl, AllowedUrlError> {
+        let url = Url::options()
+            .base_url(base_url)
+            .parse(href)
+            .map_err(AllowedUrlError::HrefParseError)?;
+
+        // Allow loads of data: from any location
+        if url.scheme() == "data" {
+            return Ok(AllowedUrl(url));
+        }
+
+        // All other sources require a base url
+        if base_url.is_none() {
+            return Err(AllowedUrlError::BaseRequired);
+        }
+
+        let base_url = base_url.unwrap();
+
+        // Deny loads from differing URI schemes
+        if url.scheme() != base_url.scheme() {
+            return Err(AllowedUrlError::DifferentURISchemes);
+        }
+
+        // resource: is allowed to load anything from other resources
+        if url.scheme() == "resource" {
+            return Ok(AllowedUrl(url));
+        }
+
+        // Non-file: isn't allowed to load anything
+        if url.scheme() != "file" {
+            return Err(AllowedUrlError::DisallowedScheme);
+        }
+
+        // We have two file: URIs.  Now canonicalize them (remove .. and symlinks, etc.)
+        // and see if the directories match
+
+        let url_path = url
+            .to_file_path()
+            .map_err(|_| AllowedUrlError::InvalidPath)?;
+        let base_path = base_url
+            .to_file_path()
+            .map_err(|_| AllowedUrlError::InvalidPath)?;
+
+        let base_parent = base_path.parent();
+        if base_parent.is_none() {
+            return Err(AllowedUrlError::BaseIsRoot);
+        }
+
+        let base_parent = base_parent.unwrap();
+
+        let url_canon =
+            canonicalize(&url_path).map_err(|_| AllowedUrlError::CanonicalizationError)?;
+        let parent_canon =
+            canonicalize(&base_parent).map_err(|_| AllowedUrlError::CanonicalizationError)?;
+
+        if url_canon.starts_with(parent_canon) {
+            Ok(AllowedUrl(url))
+        } else {
+            Err(AllowedUrlError::NotSiblingOrChildOfBaseFile)
+        }
+    }
+}
+
+// For tests, we don't want to touch the filesystem.  In that case,
+// assume that we are being passed canonical file names.
+#[cfg(not(test))]
+fn canonicalize<P: AsRef<Path>>(path: P) -> Result<PathBuf, io::Error> {
+    path.as_ref().canonicalize()
+}
+#[cfg(test)]
+fn canonicalize<P: AsRef<Path>>(path: P) -> Result<PathBuf, io::Error> {
+    Ok(path.as_ref().to_path_buf())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn disallows_relative_file_with_no_base_file() {
+        assert_eq!(
+            AllowedUrl::from_href("foo.svg", None),
+            Err(AllowedUrlError::HrefParseError(
+                url::ParseError::RelativeUrlWithoutBase
+            ))
+        );
+    }
+
+    #[test]
+    fn disallows_different_schemes() {
+        assert_eq!(
+            AllowedUrl::from_href(
+                "file:///etc/passwd",
+                Some(Url::parse("http://example.com/malicious.svg";).unwrap()).as_ref()
+            ),
+            Err(AllowedUrlError::DifferentURISchemes)
+        );
+    }
+
+    #[test]
+    fn disallows_base_is_root() {
+        assert_eq!(
+            AllowedUrl::from_href("foo.svg", Some(Url::parse("file:///").unwrap()).as_ref()),
+            Err(AllowedUrlError::BaseIsRoot)
+        );
+    }
+
+    #[test]
+    fn disallows_non_file_scheme() {
+        assert_eq!(
+            AllowedUrl::from_href(
+                "foo.svg",
+                Some(Url::parse("http://foo.bar/baz.svg";).unwrap()).as_ref()
+            ),
+            Err(AllowedUrlError::DisallowedScheme)
+        );
+    }
+
+    #[test]
+    fn allows_data_url_with_no_base_file() {
+        assert_eq!(
+            AllowedUrl::from_href("data:image/jpeg;base64,xxyyzz", None)
+                .unwrap()
+                .0,
+            Url::parse("data:image/jpeg;base64,xxyyzz").unwrap(),
+        );
+    }
+
+    #[test]
+    fn allows_relative() {
+        assert_eq!(
+            AllowedUrl::from_href(
+                "foo.svg",
+                Some(Url::parse("file:///example/bar.svg").unwrap()).as_ref()
+            )
+            .unwrap()
+            .0,
+            Url::parse("file:///example/foo.svg").unwrap(),
+        );
+    }
+
+    #[test]
+    fn allows_sibling() {
+        assert_eq!(
+            AllowedUrl::from_href(
+                "file:///example/foo.svg",
+                Some(Url::parse("file:///example/bar.svg").unwrap()).as_ref()
+            )
+            .unwrap()
+            .0,
+            Url::parse("file:///example/foo.svg").unwrap(),
+        );
+    }
+
+    #[test]
+    fn allows_child_of_sibling() {
+        assert_eq!(
+            AllowedUrl::from_href(
+                "file:///example/subdir/foo.svg",
+                Some(Url::parse("file:///example/bar.svg").unwrap()).as_ref()
+            )
+            .unwrap()
+            .0,
+            Url::parse("file:///example/subdir/foo.svg").unwrap(),
+        );
+    }
+
+    #[test]
+    fn disallows_non_sibling() {
+        assert_eq!(
+            AllowedUrl::from_href(
+                "file:///etc/passwd",
+                Some(Url::parse("file:///example/bar.svg").unwrap()).as_ref()
+            ),
+            Err(AllowedUrlError::NotSiblingOrChildOfBaseFile)
+        );
+    }
+}
diff --git a/rsvg_internals/src/lib.rs b/rsvg_internals/src/lib.rs
index 6ad88a6b..1ffa5919 100644
--- a/rsvg_internals/src/lib.rs
+++ b/rsvg_internals/src/lib.rs
@@ -105,6 +105,7 @@ mod float_eq_cairo;
 #[macro_use]
 mod property_macros;
 
+mod allowed_url;
 mod aspect_ratio;
 mod attributes;
 mod bbox;


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]