1mod sanitize;
11#[doc(inline)]
12pub use sanitize::{
13 UnsafePathError, is_reserved_device_name, sanitize_path, sanitize_relative_path,
14};
15
16use std::{
17 fs, io,
18 path::{Path, PathBuf},
19};
20
21#[cfg(loom)]
22use std::fs::File;
23#[cfg(not(loom))]
24use tokio::fs::File;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32#[non_exhaustive]
33pub enum SymlinkPolicy {
34 #[default]
38 RestrictToRoot,
39 Allow,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
52#[non_exhaustive]
53pub enum CreatedFilePermissions {
54 #[default]
56 Default,
57 OwnerReadWrite,
62}
63
64pub async fn safe_open(path: impl AsRef<Path>) -> io::Result<File> {
73 OpenOptions::new().read(true).open(path).await
74}
75
76pub fn safe_open_sync(path: impl AsRef<Path>) -> io::Result<fs::File> {
78 OpenOptionsSync::new().read(true).open(path)
79}
80
81pub async fn safe_open_in(root: impl AsRef<Path>, path: impl AsRef<Path>) -> io::Result<File> {
89 OpenOptions::new()
90 .read(true)
91 .jail(root.as_ref())
92 .open(path)
93 .await
94}
95
96pub async fn safe_open_under(root: impl AsRef<Path>, path: impl AsRef<Path>) -> io::Result<File> {
102 let (root, path) = (root.as_ref(), path.as_ref());
103 let relative = path.strip_prefix(root).map_err(|_e| {
104 io::Error::new(
105 io::ErrorKind::InvalidInput,
106 "path is outside the confined root",
107 )
108 })?;
109 safe_open_in(root, relative).await
110}
111
112pub fn safe_open_under_sync(
114 root: impl AsRef<Path>,
115 path: impl AsRef<Path>,
116) -> io::Result<fs::File> {
117 let (root, path) = (root.as_ref(), path.as_ref());
118 let relative = path.strip_prefix(root).map_err(|_e| {
119 io::Error::new(
120 io::ErrorKind::InvalidInput,
121 "path is outside the confined root",
122 )
123 })?;
124 safe_open_in_sync(root, relative)
125}
126
127pub fn safe_open_in_sync(root: impl AsRef<Path>, path: impl AsRef<Path>) -> io::Result<fs::File> {
129 OpenOptionsSync::new()
130 .read(true)
131 .jail(root.as_ref())
132 .open(path)
133}
134
135pub async fn safe_path_in(root: impl AsRef<Path>, path: impl AsRef<Path>) -> io::Result<PathBuf> {
145 let root = root.as_ref();
146 let path = root.join(sanitize_relative_path(path)?);
147 let canonical_root = canonicalize_root(root).await?;
148 ensure_within_canonical_root(&canonical_root, &path).await?;
149 Ok(path)
150}
151
152pub fn safe_path_in_sync(root: impl AsRef<Path>, path: impl AsRef<Path>) -> io::Result<PathBuf> {
154 let root = root.as_ref();
155 let path = root.join(sanitize_relative_path(path)?);
156 let canonical_root = canonicalize_root_sync(root)?;
157 ensure_within_canonical_root_sync(&canonical_root, &path)?;
158 Ok(path)
159}
160
161pub async fn safe_canonicalize_in(
167 root: impl AsRef<Path>,
168 path: impl AsRef<Path>,
169) -> io::Result<PathBuf> {
170 let root = root.as_ref();
171 let path = root.join(sanitize_relative_path(path)?);
172 let canonical_root = canonicalize_root(root).await?;
173 let canonical_path = canonicalize_existing_path(&path).await?;
174 if !canonical_path.starts_with(&canonical_root) {
175 return Err(UnsafePathError::EscapesRoot.into());
176 }
177 Ok(canonical_path)
178}
179
180pub fn safe_canonicalize_in_sync(
182 root: impl AsRef<Path>,
183 path: impl AsRef<Path>,
184) -> io::Result<PathBuf> {
185 let root = root.as_ref();
186 let path = root.join(sanitize_relative_path(path)?);
187 let canonical_root = canonicalize_root_sync(root)?;
188 let canonical_path = canonicalize_existing_path_sync(&path)?;
189 if !canonical_path.starts_with(&canonical_root) {
190 return Err(UnsafePathError::EscapesRoot.into());
191 }
192 Ok(canonical_path)
193}
194
195pub async fn safe_create_dir_all_in(
201 root: impl AsRef<Path>,
202 path: impl AsRef<Path>,
203) -> io::Result<()> {
204 let root = root.as_ref();
205 let path = root.join(sanitize_relative_path(path)?);
206 let canonical_root = canonicalize_root(root).await?;
207 ensure_within_canonical_root(&canonical_root, &path).await?;
208 #[cfg(loom)]
209 fs::create_dir_all(&path)?;
210 #[cfg(not(loom))]
211 tokio::fs::create_dir_all(&path).await?;
212 ensure_within_canonical_root(&canonical_root, &path).await
213}
214
215pub fn safe_create_dir_all_in_sync(
217 root: impl AsRef<Path>,
218 path: impl AsRef<Path>,
219) -> io::Result<()> {
220 let root = root.as_ref();
221 let path = root.join(sanitize_relative_path(path)?);
222 let canonical_root = canonicalize_root_sync(root)?;
223 ensure_within_canonical_root_sync(&canonical_root, &path)?;
224 fs::create_dir_all(&path)?;
225 ensure_within_canonical_root_sync(&canonical_root, &path)
226}
227
228pub async fn safe_write_in(
235 root: impl AsRef<Path>,
236 path: impl AsRef<Path>,
237 contents: impl AsRef<[u8]>,
238) -> io::Result<()> {
239 let root = root.as_ref();
240 let path = root.join(sanitize_relative_path(path)?);
241 let contents = contents.as_ref().to_owned();
242 let canonical_root = canonicalize_root(root).await?;
243
244 if let Some(parent) = path.parent() {
245 ensure_within_canonical_root(&canonical_root, parent).await?;
246 #[cfg(loom)]
247 fs::create_dir_all(parent)?;
248 #[cfg(not(loom))]
249 tokio::fs::create_dir_all(parent).await?;
250 ensure_within_canonical_root(&canonical_root, parent).await?;
251 }
252
253 ensure_within_canonical_root(&canonical_root, &path).await?;
254 #[cfg(loom)]
255 fs::write(&path, contents)?;
256 #[cfg(not(loom))]
257 tokio::fs::write(&path, contents).await?;
258 ensure_within_canonical_root(&canonical_root, &path).await
259}
260
261pub fn safe_write_in_sync(
263 root: impl AsRef<Path>,
264 path: impl AsRef<Path>,
265 contents: impl AsRef<[u8]>,
266) -> io::Result<()> {
267 let root = root.as_ref();
268 let path = root.join(sanitize_relative_path(path)?);
269 let canonical_root = canonicalize_root_sync(root)?;
270
271 if let Some(parent) = path.parent() {
272 ensure_within_canonical_root_sync(&canonical_root, parent)?;
273 fs::create_dir_all(parent)?;
274 ensure_within_canonical_root_sync(&canonical_root, parent)?;
275 }
276
277 ensure_within_canonical_root_sync(&canonical_root, &path)?;
278 fs::write(&path, contents)?;
279 ensure_within_canonical_root_sync(&canonical_root, &path)
280}
281
282#[derive(Debug, Clone)]
289pub struct OpenOptions {
290 inner: OpenOptionsInner,
291}
292
293impl OpenOptions {
294 #[must_use]
298 pub fn new() -> Self {
299 Self {
300 inner: OpenOptionsInner::new(),
301 }
302 }
303
304 pub fn read(&mut self, read: bool) -> &mut Self {
306 self.inner.read = read;
307 self
308 }
309
310 pub fn write(&mut self, write: bool) -> &mut Self {
312 self.inner.write = write;
313 self
314 }
315
316 pub fn append(&mut self, append: bool) -> &mut Self {
318 self.inner.append = append;
319 self
320 }
321
322 pub fn truncate(&mut self, truncate: bool) -> &mut Self {
324 self.inner.truncate = truncate;
325 self
326 }
327
328 pub fn create(&mut self, create: bool) -> &mut Self {
330 self.inner.create = create;
331 self
332 }
333
334 pub fn create_new(&mut self, create_new: bool) -> &mut Self {
336 self.inner.create_new = create_new;
337 self
338 }
339
340 pub fn jail(&mut self, root: impl Into<PathBuf>) -> &mut Self {
347 self.inner.jail = Some(root.into());
348 self
349 }
350
351 pub fn symlinks(&mut self, policy: SymlinkPolicy) -> &mut Self {
356 self.inner.symlinks = policy;
357 self
358 }
359
360 pub fn created_file_permissions(&mut self, permissions: CreatedFilePermissions) -> &mut Self {
362 self.inner.created_file_permissions = permissions;
363 self
364 }
365
366 pub async fn open(&self, path: impl AsRef<Path>) -> io::Result<File> {
369 let path = self.inner.resolve(path.as_ref()).await?;
370 #[cfg(loom)]
371 let file = self.inner.std_options().open(&path)?;
372 #[cfg(not(loom))]
373 let file = self.inner.tokio_options().open(&path).await?;
374 if let Some(root) = &self.inner.jail
375 && self.inner.symlinks == SymlinkPolicy::RestrictToRoot
376 {
377 ensure_within_root(root, &path).await?;
378 }
379 Ok(file)
380 }
381}
382
383impl Default for OpenOptions {
384 fn default() -> Self {
385 Self::new()
386 }
387}
388
389#[derive(Debug, Clone)]
393pub struct OpenOptionsSync {
394 inner: OpenOptionsInner,
395}
396
397impl OpenOptionsSync {
398 #[must_use]
402 pub fn new() -> Self {
403 Self {
404 inner: OpenOptionsInner::new(),
405 }
406 }
407
408 pub fn read(&mut self, read: bool) -> &mut Self {
410 self.inner.read = read;
411 self
412 }
413
414 pub fn write(&mut self, write: bool) -> &mut Self {
416 self.inner.write = write;
417 self
418 }
419
420 pub fn append(&mut self, append: bool) -> &mut Self {
422 self.inner.append = append;
423 self
424 }
425
426 pub fn truncate(&mut self, truncate: bool) -> &mut Self {
428 self.inner.truncate = truncate;
429 self
430 }
431
432 pub fn create(&mut self, create: bool) -> &mut Self {
434 self.inner.create = create;
435 self
436 }
437
438 pub fn create_new(&mut self, create_new: bool) -> &mut Self {
440 self.inner.create_new = create_new;
441 self
442 }
443
444 pub fn jail(&mut self, root: impl Into<PathBuf>) -> &mut Self {
446 self.inner.jail = Some(root.into());
447 self
448 }
449
450 pub fn symlinks(&mut self, policy: SymlinkPolicy) -> &mut Self {
455 self.inner.symlinks = policy;
456 self
457 }
458
459 pub fn created_file_permissions(&mut self, permissions: CreatedFilePermissions) -> &mut Self {
461 self.inner.created_file_permissions = permissions;
462 self
463 }
464
465 pub fn open(&self, path: impl AsRef<Path>) -> io::Result<fs::File> {
468 let path = self.inner.resolve_sync(path.as_ref())?;
469 let file = self.inner.std_options().open(&path)?;
470 if let Some(root) = &self.inner.jail
471 && self.inner.symlinks == SymlinkPolicy::RestrictToRoot
472 {
473 ensure_within_root_sync(root, &path)?;
474 }
475 Ok(file)
476 }
477}
478
479impl Default for OpenOptionsSync {
480 fn default() -> Self {
481 Self::new()
482 }
483}
484
485#[derive(Debug, Clone)]
486struct OpenOptionsInner {
487 read: bool,
488 write: bool,
489 append: bool,
490 truncate: bool,
491 create: bool,
492 create_new: bool,
493 jail: Option<PathBuf>,
494 symlinks: SymlinkPolicy,
495 created_file_permissions: CreatedFilePermissions,
496}
497
498impl OpenOptionsInner {
499 fn new() -> Self {
500 Self {
501 read: false,
502 write: false,
503 append: false,
504 truncate: false,
505 create: false,
506 create_new: false,
507 jail: None,
508 symlinks: SymlinkPolicy::RestrictToRoot,
509 created_file_permissions: CreatedFilePermissions::Default,
510 }
511 }
512
513 #[cfg(not(loom))]
514 fn tokio_options(&self) -> tokio::fs::OpenOptions {
515 let mut opts = tokio::fs::OpenOptions::new();
516 opts.read(self.read)
517 .write(self.write)
518 .append(self.append)
519 .truncate(self.truncate)
520 .create(self.create)
521 .create_new(self.create_new);
522 #[cfg(unix)]
523 if self.created_file_permissions == CreatedFilePermissions::OwnerReadWrite {
524 opts.mode(0o600);
525 }
526 opts
527 }
528
529 fn std_options(&self) -> fs::OpenOptions {
530 let mut opts = fs::OpenOptions::new();
531 opts.read(self.read)
532 .write(self.write)
533 .append(self.append)
534 .truncate(self.truncate)
535 .create(self.create)
536 .create_new(self.create_new);
537 #[cfg(unix)]
538 if self.created_file_permissions == CreatedFilePermissions::OwnerReadWrite {
539 use std::os::unix::fs::OpenOptionsExt as _;
540 opts.mode(0o600);
541 }
542 opts
543 }
544
545 async fn resolve(&self, path: &Path) -> io::Result<PathBuf> {
546 match &self.jail {
547 None => Ok(sanitize_path(path)?),
548 Some(root) => {
549 let full = root.join(sanitize_relative_path(path)?);
550 if self.symlinks == SymlinkPolicy::RestrictToRoot {
551 ensure_within_root(root, &full).await?;
552 }
553 Ok(full)
554 }
555 }
556 }
557
558 fn resolve_sync(&self, path: &Path) -> io::Result<PathBuf> {
559 match &self.jail {
560 None => Ok(sanitize_path(path)?),
561 Some(root) => {
562 let full = root.join(sanitize_relative_path(path)?);
563 if self.symlinks == SymlinkPolicy::RestrictToRoot {
564 ensure_within_root_sync(root, &full)?;
565 }
566 Ok(full)
567 }
568 }
569 }
570}
571
572async fn ensure_within_root(root: &Path, target: &Path) -> io::Result<()> {
579 let canonical_root = canonicalize_root(root).await?;
580 ensure_within_canonical_root(&canonical_root, target).await
581}
582
583#[cfg(loom)]
584async fn canonicalize_root(root: &Path) -> io::Result<PathBuf> {
585 canonicalize_root_sync(root)
586}
587
588#[cfg(not(loom))]
589async fn canonicalize_root(root: &Path) -> io::Result<PathBuf> {
590 tokio::fs::canonicalize(root).await
591}
592
593#[cfg(loom)]
594async fn ensure_within_canonical_root(canonical_root: &Path, target: &Path) -> io::Result<()> {
595 ensure_within_canonical_root_sync(canonical_root, target)
596}
597
598#[cfg(not(loom))]
599async fn ensure_within_canonical_root(canonical_root: &Path, target: &Path) -> io::Result<()> {
600 if let Some(existing) = nearest_existing_ancestor(target).await {
601 let canonical_target = canonicalize_existing_path(&existing).await?;
602 if !canonical_target.starts_with(canonical_root) {
603 return Err(UnsafePathError::EscapesRoot.into());
604 }
605 }
606 Ok(())
607}
608
609fn ensure_within_root_sync(root: &Path, target: &Path) -> io::Result<()> {
610 let canonical_root = canonicalize_root_sync(root)?;
611 ensure_within_canonical_root_sync(&canonical_root, target)
612}
613
614fn canonicalize_root_sync(root: &Path) -> io::Result<PathBuf> {
615 fs::canonicalize(root)
616}
617
618fn ensure_within_canonical_root_sync(canonical_root: &Path, target: &Path) -> io::Result<()> {
619 if let Some(existing) = nearest_existing_ancestor_sync(target) {
620 let canonical_target = canonicalize_existing_path_sync(&existing)?;
621 if !canonical_target.starts_with(canonical_root) {
622 return Err(UnsafePathError::EscapesRoot.into());
623 }
624 }
625 Ok(())
626}
627
628#[cfg(not(loom))]
629async fn canonicalize_existing_path(path: &Path) -> io::Result<PathBuf> {
630 match tokio::fs::canonicalize(path).await {
631 Ok(path) => Ok(path),
632 Err(err) if err.kind() == io::ErrorKind::NotFound => {
633 if tokio::fs::symlink_metadata(path).await.is_ok() {
634 return Err(UnsafePathError::EscapesRoot.into());
635 }
636 Err(err)
637 }
638 Err(err) => Err(err),
639 }
640}
641
642#[cfg(loom)]
643async fn canonicalize_existing_path(path: &Path) -> io::Result<PathBuf> {
644 canonicalize_existing_path_sync(path)
645}
646
647fn canonicalize_existing_path_sync(path: &Path) -> io::Result<PathBuf> {
648 match fs::canonicalize(path) {
649 Ok(path) => Ok(path),
650 Err(err) if err.kind() == io::ErrorKind::NotFound => {
651 if fs::symlink_metadata(path).is_ok() {
652 return Err(UnsafePathError::EscapesRoot.into());
653 }
654 Err(err)
655 }
656 Err(err) => Err(err),
657 }
658}
659
660#[cfg(not(loom))]
662async fn nearest_existing_ancestor(path: &Path) -> Option<PathBuf> {
663 let mut current = Some(path);
664 while let Some(candidate) = current {
665 if tokio::fs::symlink_metadata(candidate).await.is_ok() {
666 return Some(candidate.to_path_buf());
667 }
668 current = candidate.parent();
669 }
670 None
671}
672
673fn nearest_existing_ancestor_sync(path: &Path) -> Option<PathBuf> {
674 let mut current = Some(path);
675 while let Some(candidate) = current {
676 if fs::symlink_metadata(candidate).is_ok() {
677 return Some(candidate.to_path_buf());
678 }
679 current = candidate.parent();
680 }
681 None
682}
683
684#[cfg(all(test, not(loom)))]
685mod tests {
686 use super::*;
687 use tokio::io::AsyncReadExt;
688
689 async fn read_to_string(mut file: File) -> io::Result<String> {
690 let mut buf = String::new();
691 file.read_to_string(&mut buf).await?;
692 Ok(buf)
693 }
694
695 fn err_kind<T>(result: io::Result<T>) -> Option<io::ErrorKind> {
696 result.err().map(|err| err.kind())
697 }
698
699 #[tokio::test]
700 async fn safe_open_reads_a_regular_file() {
701 let dir = tempfile::tempdir().unwrap();
702 let path = dir.path().join("hello.txt");
703 tokio::fs::write(&path, b"hello world").await.unwrap();
704
705 let file = safe_open(&path).await.unwrap();
706 assert_eq!(read_to_string(file).await.unwrap(), "hello world");
707 }
708
709 #[test]
710 fn safe_open_sync_reads_a_regular_file() {
711 let dir = tempfile::tempdir().unwrap();
712 let path = dir.path().join("hello.txt");
713 fs::write(&path, b"hello world").unwrap();
714
715 let text = {
716 let mut s = String::new();
717 use std::io::Read as _;
718 safe_open_sync(&path)
719 .unwrap()
720 .read_to_string(&mut s)
721 .unwrap();
722 s
723 };
724 assert_eq!(text, "hello world");
725 }
726
727 #[tokio::test]
728 async fn safe_open_rejects_parent_dir_traversal() {
729 let dir = tempfile::tempdir().unwrap();
730 let path = dir.path().join("sub/../secret.txt");
731 assert_eq!(
732 err_kind(safe_open(&path).await),
733 Some(io::ErrorKind::InvalidInput),
734 );
735 }
736
737 #[tokio::test]
738 async fn safe_open_missing_file_is_not_found() {
739 let dir = tempfile::tempdir().unwrap();
740 let path = dir.path().join("does-not-exist.txt");
741 assert_eq!(
742 err_kind(safe_open(&path).await),
743 Some(io::ErrorKind::NotFound),
744 );
745 }
746
747 #[tokio::test]
748 async fn safe_open_in_serves_files_within_root() {
749 let root = tempfile::tempdir().unwrap();
750 tokio::fs::create_dir(root.path().join("assets"))
751 .await
752 .unwrap();
753 tokio::fs::write(root.path().join("assets/app.js"), b"console.log(1)")
754 .await
755 .unwrap();
756
757 let file = safe_open_in(root.path(), "assets/app.js").await.unwrap();
758 assert_eq!(read_to_string(file).await.unwrap(), "console.log(1)");
759
760 let file = safe_open_in(root.path(), "/assets/app.js").await;
761 assert_eq!(err_kind(file), Some(io::ErrorKind::InvalidInput));
762 }
763
764 #[tokio::test]
765 async fn safe_open_in_rejects_traversal_out_of_root() {
766 let parent = tempfile::tempdir().unwrap();
767 tokio::fs::write(parent.path().join("secret.txt"), b"top secret")
768 .await
769 .unwrap();
770 let root = parent.path().join("public");
771 tokio::fs::create_dir(&root).await.unwrap();
772 tokio::fs::write(root.join("index.html"), b"<h1>hi</h1>")
773 .await
774 .unwrap();
775
776 safe_open_in(&root, "index.html").await.unwrap();
777
778 for payload in ["../secret.txt", "../../etc/passwd", "..\\secret.txt"] {
779 let result = safe_open_in(&root, payload).await;
780 assert!(
781 result.is_err(),
782 "expected `{payload}` to be rejected, got Ok",
783 );
784 }
785 }
786
787 #[cfg(unix)]
788 #[tokio::test]
789 async fn safe_open_in_rejects_symlink_escaping_root() {
790 let parent = tempfile::tempdir().unwrap();
791 tokio::fs::write(parent.path().join("secret.txt"), b"top secret")
792 .await
793 .unwrap();
794 let root = parent.path().join("public");
795 tokio::fs::create_dir(&root).await.unwrap();
796
797 std::os::unix::fs::symlink(parent.path().join("secret.txt"), root.join("escape")).unwrap();
798
799 let result = safe_open_in(&root, "escape").await;
800 assert_eq!(err_kind(result), Some(io::ErrorKind::InvalidInput));
801 }
802
803 #[cfg(unix)]
804 #[tokio::test]
805 async fn safe_open_in_allows_symlink_within_root() {
806 let root = tempfile::tempdir().unwrap();
807 tokio::fs::write(root.path().join("real.txt"), b"data")
808 .await
809 .unwrap();
810 std::os::unix::fs::symlink(root.path().join("real.txt"), root.path().join("link")).unwrap();
811
812 let file = safe_open_in(root.path(), "link").await.unwrap();
813 assert_eq!(read_to_string(file).await.unwrap(), "data");
814 }
815
816 #[cfg(unix)]
817 #[tokio::test]
818 async fn jail_create_rejects_dangling_symlink_escape() {
819 let parent = tempfile::tempdir().unwrap();
820 let root = parent.path().join("public");
821 let outside = parent.path().join("outside");
822 tokio::fs::create_dir(&root).await.unwrap();
823 tokio::fs::create_dir(&outside).await.unwrap();
824
825 let outside_target = outside.join("created.txt");
826 std::os::unix::fs::symlink(&outside_target, root.join("upload.txt")).unwrap();
827
828 let result = OpenOptions::new()
829 .write(true)
830 .create(true)
831 .jail(&root)
832 .open("upload.txt")
833 .await;
834
835 assert_eq!(err_kind(result), Some(io::ErrorKind::InvalidInput));
836 assert!(!outside_target.exists());
837 }
838
839 #[cfg(unix)]
840 #[test]
841 fn jail_create_sync_rejects_dangling_symlink_escape() {
842 let parent = tempfile::tempdir().unwrap();
843 let root = parent.path().join("public");
844 let outside = parent.path().join("outside");
845 fs::create_dir(&root).unwrap();
846 fs::create_dir(&outside).unwrap();
847
848 let outside_target = outside.join("created.txt");
849 std::os::unix::fs::symlink(&outside_target, root.join("upload.txt")).unwrap();
850
851 let result = OpenOptionsSync::new()
852 .write(true)
853 .create(true)
854 .jail(&root)
855 .open("upload.txt");
856
857 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
858 assert!(!outside_target.exists());
859 }
860
861 #[cfg(unix)]
862 #[tokio::test]
863 async fn jail_allow_symlinks_follows_escaping_link_but_keeps_lexical_guard() {
864 let parent = tempfile::tempdir().unwrap();
865 tokio::fs::write(parent.path().join("secret.txt"), b"top secret")
866 .await
867 .unwrap();
868 let root = parent.path().join("public");
869 tokio::fs::create_dir(&root).await.unwrap();
870 std::os::unix::fs::symlink(parent.path().join("secret.txt"), root.join("escape")).unwrap();
871
872 assert_eq!(
873 err_kind(safe_open_in(&root, "escape").await),
874 Some(io::ErrorKind::InvalidInput),
875 );
876
877 let file = OpenOptions::new()
878 .read(true)
879 .jail(&root)
880 .symlinks(SymlinkPolicy::Allow)
881 .open("escape")
882 .await
883 .unwrap();
884 assert_eq!(read_to_string(file).await.unwrap(), "top secret");
885
886 let traversal = OpenOptions::new()
887 .read(true)
888 .jail(&root)
889 .symlinks(SymlinkPolicy::Allow)
890 .open("../secret.txt")
891 .await;
892 assert_eq!(err_kind(traversal), Some(io::ErrorKind::InvalidInput));
893 }
894
895 #[tokio::test]
896 async fn open_options_can_create_within_jail() {
897 let root = tempfile::tempdir().unwrap();
898 OpenOptions::new()
899 .write(true)
900 .create(true)
901 .jail(root.path())
902 .open("nested/created.txt")
903 .await
904 .expect_err("parent dir does not exist yet");
905
906 tokio::fs::create_dir(root.path().join("nested"))
907 .await
908 .unwrap();
909 let _file = OpenOptions::new()
910 .write(true)
911 .create(true)
912 .jail(root.path())
913 .open("nested/created.txt")
914 .await
915 .unwrap();
916 assert!(root.path().join("nested/created.txt").exists());
917 }
918
919 #[cfg(unix)]
920 #[tokio::test]
921 async fn open_options_private_created_file_has_no_group_or_other_bits() {
922 use std::os::unix::fs::PermissionsExt as _;
923
924 let root = tempfile::tempdir().unwrap();
925 let path = root.path().join("secret.txt");
926 let _file = OpenOptions::new()
927 .write(true)
928 .create_new(true)
929 .created_file_permissions(CreatedFilePermissions::OwnerReadWrite)
930 .open(&path)
931 .await
932 .unwrap();
933
934 let mode = tokio::fs::metadata(&path)
935 .await
936 .unwrap()
937 .permissions()
938 .mode();
939 assert_eq!(mode & 0o077, 0);
940 }
941
942 #[cfg(unix)]
943 #[test]
944 fn open_options_sync_private_created_file_has_no_group_or_other_bits() {
945 use std::os::unix::fs::PermissionsExt as _;
946
947 let root = tempfile::tempdir().unwrap();
948 let path = root.path().join("secret.txt");
949 let _file = OpenOptionsSync::new()
950 .write(true)
951 .create_new(true)
952 .created_file_permissions(CreatedFilePermissions::OwnerReadWrite)
953 .open(&path)
954 .unwrap();
955
956 let mode = fs::metadata(&path).unwrap().permissions().mode();
957 assert_eq!(mode & 0o077, 0);
958 }
959
960 #[tokio::test]
961 async fn safe_write_in_creates_parent_dirs() {
962 let root = tempfile::tempdir().unwrap();
963 safe_write_in(root.path(), "nested/file.txt", b"hello")
964 .await
965 .unwrap();
966 assert_eq!(
967 tokio::fs::read_to_string(root.path().join("nested/file.txt"))
968 .await
969 .unwrap(),
970 "hello",
971 );
972 }
973
974 #[test]
975 fn safe_write_in_sync_creates_parent_dirs() {
976 let root = tempfile::tempdir().unwrap();
977 safe_write_in_sync(root.path(), "nested/file.txt", b"hello").unwrap();
978 assert_eq!(
979 fs::read_to_string(root.path().join("nested/file.txt")).unwrap(),
980 "hello",
981 );
982 }
983
984 #[test]
985 fn safe_write_in_sync_rejects_traversal() {
986 let root = tempfile::tempdir().unwrap();
987 let err = safe_write_in_sync(root.path(), "../escape.txt", b"nope").unwrap_err();
988 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
989 assert!(!root.path().parent().unwrap().join("escape.txt").exists());
990 }
991
992 #[tokio::test]
993 async fn safe_create_dir_all_in_rejects_absolute_paths() {
994 let root = tempfile::tempdir().unwrap();
995 let err = safe_create_dir_all_in(root.path(), "/tmp/escape")
996 .await
997 .unwrap_err();
998 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
999 }
1000
1001 #[test]
1002 fn safe_create_dir_all_in_sync_rejects_absolute_paths() {
1003 let root = tempfile::tempdir().unwrap();
1004 let err = safe_create_dir_all_in_sync(root.path(), "/tmp/escape").unwrap_err();
1005 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1006 }
1007
1008 #[test]
1009 fn safe_path_in_sync_resolves_plain_relative_path() {
1010 let root = tempfile::tempdir().unwrap();
1011 assert_eq!(
1012 safe_path_in_sync(root.path(), "nested/file.txt").unwrap(),
1013 root.path().join("nested/file.txt"),
1014 );
1015 }
1016
1017 #[tokio::test]
1018 async fn safe_canonicalize_in_resolves_existing_path() {
1019 let root = tempfile::tempdir().unwrap();
1020 tokio::fs::create_dir(root.path().join("nested"))
1021 .await
1022 .unwrap();
1023 assert_eq!(
1024 safe_canonicalize_in(root.path(), "nested").await.unwrap(),
1025 root.path().canonicalize().unwrap().join("nested"),
1026 );
1027 }
1028
1029 #[test]
1030 fn safe_canonicalize_in_sync_resolves_existing_path() {
1031 let root = tempfile::tempdir().unwrap();
1032 fs::create_dir(root.path().join("nested")).unwrap();
1033 assert_eq!(
1034 safe_canonicalize_in_sync(root.path(), "nested").unwrap(),
1035 root.path().canonicalize().unwrap().join("nested"),
1036 );
1037 }
1038
1039 #[cfg(unix)]
1040 #[test]
1041 fn safe_write_in_sync_rejects_symlink_escape() {
1042 let parent = tempfile::tempdir().unwrap();
1043 let root = parent.path().join("root");
1044 let outside = parent.path().join("outside");
1045 fs::create_dir(&root).unwrap();
1046 fs::create_dir(&outside).unwrap();
1047 std::os::unix::fs::symlink(outside.join("created.txt"), root.join("link")).unwrap();
1048
1049 let err = safe_write_in_sync(&root, "link", b"nope").unwrap_err();
1050 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1051 assert!(!outside.join("created.txt").exists());
1052 }
1053
1054 #[cfg(unix)]
1055 #[test]
1056 fn safe_create_dir_all_in_sync_rejects_symlink_escape() {
1057 let parent = tempfile::tempdir().unwrap();
1058 let root = parent.path().join("root");
1059 let outside = parent.path().join("outside");
1060 fs::create_dir(&root).unwrap();
1061 fs::create_dir(&outside).unwrap();
1062 std::os::unix::fs::symlink(&outside, root.join("link")).unwrap();
1063
1064 let err = safe_create_dir_all_in_sync(&root, "link/nested").unwrap_err();
1065 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1066 assert!(!outside.join("nested").exists());
1067 }
1068
1069 #[cfg(unix)]
1070 #[test]
1071 fn safe_canonicalize_in_sync_rejects_symlink_escape() {
1072 let parent = tempfile::tempdir().unwrap();
1073 let root = parent.path().join("root");
1074 let outside = parent.path().join("outside");
1075 fs::create_dir(&root).unwrap();
1076 fs::create_dir(&outside).unwrap();
1077 std::os::unix::fs::symlink(&outside, root.join("link")).unwrap();
1078
1079 let err = safe_canonicalize_in_sync(&root, "link").unwrap_err();
1080 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1081 }
1082
1083 #[cfg(unix)]
1084 #[tokio::test]
1085 async fn safe_canonicalize_in_rejects_symlink_escape() {
1086 let parent = tempfile::tempdir().unwrap();
1087 let root = parent.path().join("root");
1088 let outside = parent.path().join("outside");
1089 tokio::fs::create_dir(&root).await.unwrap();
1090 tokio::fs::create_dir(&outside).await.unwrap();
1091 std::os::unix::fs::symlink(&outside, root.join("link")).unwrap();
1092
1093 let err = safe_canonicalize_in(&root, "link").await.unwrap_err();
1094 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1095 }
1096}