The CWD command allows the user to change its current folder location. However, it's far from easy to do.
Before going into the implementation of this command, we'll need to discuss a potential security issue: paths.
Imagine the user is at the "/" location (which will corresponds to, say, /home/someone/somewhere) and requests foo/../../. If we just accept the path and move the user to this location, it'll end up at /home/someone. This means that the users could access all of your computer without issue. You see the problem now?
Luckily for us, Rust has a nice method on Path that allows us to fix this huge security issue. I'm talking about Path::canonicalize (which is an alias of the fs::canonicalize function).
So, what does this function do? Let's take an example:
let path = Path::new("/foo/test/../bar.rs"); assert_eq!(path.canonicalize().unwrap(), PathBuf::from("/foo/bar.rs"));
As you can see, it interprets the path, normalizes everything (.. removes the folder component), and resolves symbolic links as well. Quite magical, right?
Of course, all good things have a downside, and so does canonicalize.: it can only work on real paths. If a part of the path doesn't exist, the function will just fail. It's pretty easy to get through it when you know it, but it can sound surprising at first.
So, how do we fix this? Well, we need to play with a real path. So first, we need to append the user's server path to the real server path (the one it has on the computer). Once this is done, we just append the path requested by the user and call canonicalize.
That's not very complicated, but is a bit annoying to play with at first. Don't worry, though, the code is coming!
If you wonder why we're not just using the chroot function (which would solve all problems), remember that this FTP server is supposed to work on every platform.
So first, let's add a new command entry to the enum Command :
Cwd(PathBuf),
Good, now let's add it to the Command::new method matching:
b"CWD" => Command::Cwd(data.map(|bytes| Path::new(str::from_utf8(bytes).unwrap()).to_path_buf()).unwrap()),
Perfect! I'll let you add it into the AsRef implementation as well. Now it's time to go into the real implementation:
Command::Cwd(directory) => self.cwd(directory),
For once, to make our life easier, we'll create a new method in our Client, so all the code from the CWD command won't fill the enum:
fn complete_path(&self, path: PathBuf, server_root: &PathBuf) -> Result<PathBuf, io::Error> { let directory = server_root.join(if path.has_root() { path.iter().skip(1).collect() } else { path }); let dir = directory.canonicalize(); if let Ok(ref dir) = dir { if !dir.starts_with(&server_root) { return Err(io::ErrorKind::PermissionDenied.into()); } } dir } fn cwd(mut self, directory: PathBuf) { let server_root = env::current_dir().unwrap(); let path = self.cwd.join(&directory); if let Ok(dir) = self.complete_path(path, &server_root) { if let Ok(prefix) = dir.strip_prefix(&server_root) .map(|p| p.to_path_buf()) { self.cwd = prefix.to_path_buf(); send_cmd(&mut self.stream, ResultCode::Ok, &format!("Directory changed to \"{}\"", directory.display())); return } } send_cmd(&mut self.stream, ResultCode::FileNotFound, "No such file or directory"); }
OK, that's a lot of code. Let's now go through the execution flow:
let server_root = env::current_dir().unwrap();
For now, you can't set which folder the server is running on; it'll be changed later on:
let path = self.cwd.join(&directory);
First, we join the requested directory to the current directory of the user:
if let Ok(dir) = self.complete_path(path, &server_root) {
Things start to get funny in here. The whole canonicalization process is in there.
Now let's append the user path to the (real) server path:
let directory = server_root.join(if path.has_root() { path.iter().skip(1).collect() } else { path });
So, if the path is an absolute one (starting with "/" on Unix or a prefix on Windows such as c:), we need to remove the first component of the path, otherwise, we just append it.
We now have a full and potentially existent path. Let's canonicalize it:
let dir = directory.canonicalize();
Now we have one more thing to check—if the path doesn't start with the server root, then it means that the user tried to cheat on us and tried to access non-accessible folders. Here is how we do it:
if let Ok(ref dir) = dir { if !dir.starts_with(&server_root) { return Err(io::ErrorKind::PermissionDenied.into()); } }
In the case that canonicalize returned an error, there's no need to check if it did (since it's already an error). If it succeeded but doesn't start with server_root, then we return an error.
That's it for this function. Now, we'll return the result to the caller and can go back to the cwd method:
if let Ok(dir) = self.complete_path(path, &server_root) { if let Ok(prefix) = dir.strip_prefix(&server_root) .map(|p| p.to_path_buf()) { // ... } }
Once we get the full directory path and have confirmed it was okay, we need to remove the server_root prefix to get the path from our server root:
self.cwd = prefix.to_path_buf(); send_cmd(&mut self.stream, ResultCode::Ok, &format!("Directory changed to \"{}\"", directory.display())); return
Finally, once this is done, we can just set the path to the user and send back a message that the command succeeded (and return to avoid sending back that we failed!).
If anything goes wrong, we send back the following:
send_cmd(&mut self.stream, ResultCode::FileNotFound, "No such file or directory");
That's it for this command! You now know how to avoid a security issue by checking received paths provided by the clients.