Skip to main content

actix_files/
path_buf.rs

1use std::{
2    path::{Component, Path, PathBuf},
3    str::FromStr,
4};
5
6use actix_utils::future::{ready, Ready};
7use actix_web::{dev::Payload, FromRequest, HttpRequest};
8
9use crate::error::UriSegmentError;
10
11/// Secure Path Traversal Guard
12///
13/// This struct parses a request-uri [`PathBuf`](std::path::PathBuf)
14#[derive(Debug, PartialEq, Eq)]
15pub struct PathBufWrap(PathBuf);
16
17impl FromStr for PathBufWrap {
18    type Err = UriSegmentError;
19
20    fn from_str(path: &str) -> Result<Self, Self::Err> {
21        Self::parse_path(path, false)
22    }
23}
24
25impl PathBufWrap {
26    /// Parse a safe path from the unprocessed tail of a supplied
27    /// [`HttpRequest`](actix_web::HttpRequest), given the choice of allowing hidden files to be
28    /// considered valid segments.
29    ///
30    /// This uses [`HttpRequest::match_info`](actix_web::HttpRequest::match_info) and
31    /// [`Path::unprocessed`](actix_web::dev::Path::unprocessed), which returns the part of the
32    /// path not matched by route patterns. This is useful for mounted services (eg. `Files`),
33    /// where only the tail should be parsed.
34    ///
35    /// Path traversal is guarded by this method.
36    #[inline]
37    pub fn parse_unprocessed_req(
38        req: &HttpRequest,
39        hidden_files: bool,
40    ) -> Result<Self, UriSegmentError> {
41        Self::parse_path(req.match_info().unprocessed(), hidden_files)
42    }
43
44    /// Parse a safe path from the full request path of a supplied
45    /// [`HttpRequest`](actix_web::HttpRequest), given the choice of allowing hidden files to be
46    /// considered valid segments.
47    ///
48    /// This uses [`HttpRequest::path`](actix_web::HttpRequest::path), and is more appropriate
49    /// for non-mounted handlers that want the entire request path.
50    ///
51    /// Path traversal is guarded by this method.
52    #[inline]
53    pub fn parse_req_path(req: &HttpRequest, hidden_files: bool) -> Result<Self, UriSegmentError> {
54        Self::parse_path(req.path(), hidden_files)
55    }
56
57    /// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
58    ///
59    /// Path traversal is guarded by this method.
60    pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
61        let mut buf = PathBuf::new();
62
63        // equivalent to `path.split('/').count()`
64        let mut segment_count = path.matches('/').count() + 1;
65
66        // we can decode the whole path here (instead of per-segment decoding)
67        // because we will reject `%2F` in paths using `segment_count`.
68        let path = percent_encoding::percent_decode_str(path)
69            .decode_utf8()
70            .map_err(|_| UriSegmentError::NotValidUtf8)?;
71
72        // disallow decoding `%2F` into `/`
73        if segment_count != path.matches('/').count() + 1 {
74            return Err(UriSegmentError::BadChar('/'));
75        }
76
77        for segment in path.split('/') {
78            if segment == ".." {
79                segment_count -= 1;
80                buf.pop();
81            } else if !hidden_files && segment.starts_with('.') {
82                return Err(UriSegmentError::BadStart('.'));
83            } else if segment.starts_with('*') {
84                return Err(UriSegmentError::BadStart('*'));
85            } else if segment.ends_with(':') {
86                return Err(UriSegmentError::BadEnd(':'));
87            } else if segment.ends_with('>') {
88                return Err(UriSegmentError::BadEnd('>'));
89            } else if segment.ends_with('<') {
90                return Err(UriSegmentError::BadEnd('<'));
91            } else if segment.is_empty() {
92                segment_count -= 1;
93                continue;
94            } else if cfg!(windows) && segment.contains('\\') {
95                return Err(UriSegmentError::BadChar('\\'));
96            } else if cfg!(windows) && segment.contains(':') {
97                return Err(UriSegmentError::BadChar(':'));
98            } else {
99                buf.push(segment)
100            }
101        }
102
103        // make sure we agree with stdlib parser
104        for (i, component) in buf.components().enumerate() {
105            assert!(
106                matches!(component, Component::Normal(_)),
107                "component `{:?}` is not normal",
108                component
109            );
110            assert!(i < segment_count);
111        }
112
113        Ok(PathBufWrap(buf))
114    }
115}
116
117impl AsRef<Path> for PathBufWrap {
118    fn as_ref(&self) -> &Path {
119        self.0.as_ref()
120    }
121}
122
123impl FromRequest for PathBufWrap {
124    type Error = UriSegmentError;
125    type Future = Ready<Result<Self, Self::Error>>;
126
127    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
128        // Uses the unprocessed tail of the request path and disallows hidden files.
129        ready(req.match_info().unprocessed().parse())
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_path_buf() {
139        assert_eq!(
140            PathBufWrap::from_str("/test/.tt").map(|t| t.0),
141            Err(UriSegmentError::BadStart('.'))
142        );
143        assert_eq!(
144            PathBufWrap::from_str("/test/*tt").map(|t| t.0),
145            Err(UriSegmentError::BadStart('*'))
146        );
147        assert_eq!(
148            PathBufWrap::from_str("/test/tt:").map(|t| t.0),
149            Err(UriSegmentError::BadEnd(':'))
150        );
151        assert_eq!(
152            PathBufWrap::from_str("/test/tt<").map(|t| t.0),
153            Err(UriSegmentError::BadEnd('<'))
154        );
155        assert_eq!(
156            PathBufWrap::from_str("/test/tt>").map(|t| t.0),
157            Err(UriSegmentError::BadEnd('>'))
158        );
159        assert_eq!(
160            PathBufWrap::from_str("/seg1/seg2/").unwrap().0,
161            PathBuf::from_iter(vec!["seg1", "seg2"])
162        );
163        assert_eq!(
164            PathBufWrap::from_str("/seg1/../seg2/").unwrap().0,
165            PathBuf::from_iter(vec!["seg2"])
166        );
167    }
168
169    #[test]
170    fn test_parse_path() {
171        assert_eq!(
172            PathBufWrap::parse_path("/test/.tt", false).map(|t| t.0),
173            Err(UriSegmentError::BadStart('.'))
174        );
175
176        assert_eq!(
177            PathBufWrap::parse_path("/test/.tt", true).unwrap().0,
178            PathBuf::from_iter(vec!["test", ".tt"])
179        );
180    }
181
182    #[test]
183    fn path_traversal() {
184        assert_eq!(
185            PathBufWrap::parse_path("/../README.md", false).unwrap().0,
186            PathBuf::from_iter(vec!["README.md"])
187        );
188
189        assert_eq!(
190            PathBufWrap::parse_path("/../README.md", true).unwrap().0,
191            PathBuf::from_iter(vec!["README.md"])
192        );
193
194        assert_eq!(
195            PathBufWrap::parse_path("/../../../../../../../../../../etc/passwd", false)
196                .unwrap()
197                .0,
198            PathBuf::from_iter(vec!["etc/passwd"])
199        );
200    }
201
202    #[test]
203    #[cfg_attr(windows, should_panic)]
204    fn windows_drive_traversal() {
205        // detect issues in windows that could lead to path traversal
206        // see <https://github.com/SergioBenitez/Rocket/issues/1949
207
208        assert_eq!(
209            PathBufWrap::parse_path("C:test.txt", false).unwrap().0,
210            PathBuf::from_iter(vec!["C:test.txt"])
211        );
212
213        assert_eq!(
214            PathBufWrap::parse_path("C:../whatever", false).unwrap().0,
215            PathBuf::from_iter(vec!["C:../whatever"])
216        );
217
218        assert_eq!(
219            PathBufWrap::parse_path(":test.txt", false).unwrap().0,
220            PathBuf::from_iter(vec![":test.txt"])
221        );
222    }
223}