Repository: pyrossh/rust-embed (language: rust)

Issue:
Unable to read symbolic link in debug mode
RustEmbed::get fails to `get` a symbolic link file in debug mode, but suceeds in "release" see #235 for example


Discussion / hints:

Fix patch:
diff --git a/examples/public/symlinks/main.js b/examples/public/symlinks/main.js
new file mode 120000
index 0000000..05fdb15
--- /dev/null
+++ b/examples/public/symlinks/main.js
@@ -0,0 +1,1 @@
+../main.js
\ No newline at end of file
diff --git a/impl/src/lib.rs b/impl/src/lib.rs
index 114456b..cedb14b 100644
--- a/impl/src/lib.rs
+++ b/impl/src/lib.rs
@@ -141,7 +141,16 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ
               let canonical_file_path = file_path.canonicalize().ok()?;
               if !canonical_file_path.starts_with(#canonical_folder_path) {
                   // Tried to request a path that is not in the embedded folder
-                  return ::std::option::Option::None;
+
+                  // TODO: Currently it allows "path_traversal_attack" for the symlink files
+                  // For it to be working properly we need to get absolute path first
+                  // and check that instead if it starts with `canonical_folder_path`
+                  // https://doc.rust-lang.org/std/path/fn.absolute.html (currently nightly)
+                  // Should be allowed only if it was a symlink
+                  let metadata = ::std::fs::symlink_metadata(file_path.as_path()).ok()?;
+                  if !metadata.is_symlink() {
+                    return ::std::option::Option::None;
+                  }
               }
 
               if rust_embed::utils::is_path_included(&rel_file_path, INCLUDES, EXCLUDES) {


Test patch:
diff --git a/tests/include_exclude.rs b/tests/include_exclude.rs
index e49ca89..a0dd99d 100644
--- a/tests/include_exclude.rs
+++ b/tests/include_exclude.rs
@@ -9,7 +9,8 @@ fn get_works() {
   assert!(AllAssets::get("index.html").is_some(), "index.html should exist");
   assert!(AllAssets::get("gg.html").is_none(), "gg.html should not exist");
   assert!(AllAssets::get("images/llama.png").is_some(), "llama.png should exist");
-  assert_eq!(AllAssets::iter().count(), 6);
+  assert!(AllAssets::get("symlinks/main.js").is_some(), "main.js should exist");
+  assert_eq!(AllAssets::iter().count(), 7);
 }
 
 #[derive(RustEmbed)]
@@ -36,8 +37,9 @@ struct ExcludeSomeAssets;
 fn excluding_some_assets_works() {
   assert!(ExcludeSomeAssets::get("index.html").is_none(), "index.html should not exist");
   assert!(ExcludeSomeAssets::get("main.js").is_some(), "main.js should exist");
+  assert!(ExcludeSomeAssets::get("symlinks/main.js").is_some(), "main.js symlink should exist");
   assert!(ExcludeSomeAssets::get("images/llama.png").is_none(), "llama.png should not exist");
-  assert_eq!(ExcludeSomeAssets::iter().count(), 2);
+  assert_eq!(ExcludeSomeAssets::iter().count(), 3);
 }
 
 #[derive(RustEmbed)]
@@ -52,3 +54,15 @@ fn exclude_has_higher_priority() {
   assert!(ExcludePriorityAssets::get("images/llama.png").is_some(), "llama.png should exist");
   assert_eq!(ExcludePriorityAssets::iter().count(), 2);
 }
+
+#[derive(RustEmbed)]
+#[folder = "examples/public/symlinks"]
+#[include = "main.js"]
+struct IncludeSymlink;
+
+#[test]
+fn include_symlink() {
+  assert_eq!(IncludeSymlink::iter().count(), 1);
+  assert_eq!(IncludeSymlink::iter().next(), Some(std::borrow::Cow::Borrowed("main.js")));
+  assert!(IncludeSymlink::get("main.js").is_some())
+}
diff --git a/tests/interpolated_path.rs b/tests/interpolated_path.rs
index 0df2366..e446eef 100644
--- a/tests/interpolated_path.rs
+++ b/tests/interpolated_path.rs
@@ -19,7 +19,7 @@ fn iter_works() {
     assert!(Asset::get(file.as_ref()).is_some());
     num_files += 1;
   }
-  assert_eq!(num_files, 6);
+  assert_eq!(num_files, 7);
 }
 
 #[test]
@@ -32,6 +32,6 @@ fn trait_works_generic_helper<E: rust_embed::RustEmbed>() {
     assert!(E::get(file.as_ref()).is_some());
     num_files += 1;
   }
-  assert_eq!(num_files, 6);
+  assert_eq!(num_files, 7);
   assert!(E::get("gg.html").is_none(), "gg.html should not exist");
 }
diff --git a/tests/lib.rs b/tests/lib.rs
index a4636aa..b3d6433 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -28,7 +28,7 @@ fn iter_works() {
     assert!(Asset::get(file.as_ref()).is_some());
     num_files += 1;
   }
-  assert_eq!(num_files, 6);
+  assert_eq!(num_files, 7);
 }
 
 #[test]
@@ -41,6 +41,6 @@ fn trait_works_generic_helper<E: rust_embed::RustEmbed>() {
     assert!(E::get(file.as_ref()).is_some());
     num_files += 1;
   }
-  assert_eq!(num_files, 6);
+  assert_eq!(num_files, 7);
   assert!(E::get("gg.html").is_none(), "gg.html should not exist");
 }
diff --git a/tests/path_traversal_attack.rs b/tests/path_traversal_attack.rs
index 43e4e8b..63e9bee 100644
--- a/tests/path_traversal_attack.rs
+++ b/tests/path_traversal_attack.rs
@@ -11,3 +11,17 @@ struct Assets;
 fn path_traversal_attack_fails() {
   assert!(Assets::get("../basic.rs").is_none());
 }
+
+#[derive(RustEmbed)]
+#[folder = "examples/axum-spa/"]
+struct AxumAssets;
+
+// TODO:
+/// Prevent attempts to access symlinks outside of the embedded folder.
+/// This is mainly a concern when running in debug mode, since that loads from
+/// the file system at runtime.
+#[test]
+#[ignore = "see https://github.com/pyrossh/rust-embed/pull/235"]
+fn path_traversal_attack_symlink_fails() {
+  assert!(Assets::get("../public/symlinks/main.js").is_none());
+}