New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Userland: Fix tar/unzip directory traversal vulnerability #5713
Userland: Fix tar/unzip directory traversal vulnerability #5713
Conversation
7165ec7
to
bec265a
Compare
|
I had a quick read over the code, but haven't tested. In the if (!file_name.starts_with(current_path.to_string()))Wouldn't this still allow traversal, as a filename of The approach for I started on a patch for this ages ago (see testing code below). The issue I ran into is that diff --git a/Userland/unzip.cpp b/Userland/unzip.cpp
index 2617c353d..bb67a83bc 100644
--- a/Userland/unzip.cpp
+++ b/Userland/unzip.cpp
@@ -28,9 +28,13 @@
#include <AK/NumberFormat.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
+#include <limits.h>
+#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
+#include <unistd.h>
+bool verbose = false;
static const u8 central_directory_file_header_sig[] = "\x50\x4b\x01\x02";
static bool seek_and_read(u8* buffer, const MappedFile& file, off_t seek_to, size_t bytes_to_read)
@@ -120,31 +124,54 @@ static bool unpack_file_for_central_directory_index(off_t central_directory_inde
return false;
file_name[file_name_length] = '\0';
- if (file_name[file_name_length - 1] == '/') {
- if (mkdir(file_name, 0755) < 0) {
+ if (strlen(file_name) > PATH_MAX) {
+ fprintf(stderr, "Warning: skipped file path (length %lu exceeds %d): %s\n", strlen(file_name), PATH_MAX, file_name);
+ return false;
+ }
+
+ char output_directory[PATH_MAX];
+ // FIXME: change once output directory (-d) is supported
+ getcwd(output_directory, sizeof(output_directory));
+
+ char file_path[PATH_MAX];
+ //realpath(String::format("%s/%s", output_directory, file_name).characters(), file_path);
+ realpath(file_name, file_path);
+ printf("OUT: %s\n", output_directory);
+ printf("path: %s\n", file_path);
+
+ if (strncmp(output_directory, file_path, strlen(output_directory)) != 0) {
+ fprintf(stderr, "Warning: ignored path traversal: %s\n", file_path);
+ //fprintf(stderr, "Warning: ignored path traversal: %s\n", file_name);
+ return false;
+ }
+
+ if (verbose)
+ printf(" extracting: %s\n", file_path);
+
+ if (file_path[strlen(file_path) - 1] == '/') {
+ if (mkdir(file_path, 0755) < 0) {
perror("mkdir");
return false;
}
} else {
- auto new_file = Core::File::construct(String { file_name });
+ auto new_file = Core::File::construct(String { file_path });
if (!new_file->open(Core::IODevice::WriteOnly)) {
- fprintf(stderr, "Can't write file %s: %s\n", file_name, new_file->error_string());
+ fprintf(stderr, "Can't write file %s: %s\n", file_path, new_file->error_string());
return false;
}
- printf(" extracting: %s\n", file_name);
u8 raw_file_contents[compressed_file_size];
- if (!seek_and_read(raw_file_contents, file, local_file_header_index + LFHFileNameBaseOffset + file_name_length + extra_field_length, compressed_file_size))
+ if (!seek_and_read(raw_file_contents, file, local_file_header_index + LFHFileNameBaseOffset + strlen(file_path) + extra_field_length, compressed_file_size))
return false;
// FIXME: Try to uncompress data here. We're just ignoring it as no decompression methods are implemented yet.
if (!new_file->write(raw_file_contents, compressed_file_size)) {
- fprintf(stderr, "Can't write file contents in %s: %s\n", file_name, new_file->error_string());
+ fprintf(stderr, "Can't write file contents in %s: %s\n", file_path, new_file->error_string());
return false;
}
if (!new_file->close()) {
- fprintf(stderr, "Can't close file %s: %s\n", file_name, new_file->error_string());
+ fprintf(stderr, "Can't close file %s: %s\n", file_path, new_file->error_string());
return false;
}
}
@@ -159,6 +186,7 @@ int main(int argc, char** argv)
Core::ArgsParser args_parser;
args_parser.add_option(map_size_limit, "Maximum chunk size to map", "map-size-limit", 0, "size");
+ args_parser.add_option(verbose, "Verbose output", "verbose", 'v');
args_parser.add_positional_argument(path, "File to unzip", "path", Core::ArgsParser::Required::Yes);
args_parser.parse(argc, argv);
|
Confirmed. I tested the |
|
My testing wasn't good enough to catch the other issue you pointed out so thanks for mentioning it. I'll have to take this one back to the drawing board and see how to fix this. |
Using Unfortunately this issue is more complex than it appears on the surface. It [the unzip patch] definitely resolves the issue, but it does so by breaking existing functionality. I got about as far as you did before I decided it was someone else's problem - good luck :P |
|
We might be in luck! I have to look this over but implementing this was going to be my next idea. Looks like its already been done though (maybe still have to review) Line 41 in 54f6436
|
|
@bcoles although I think using Lexical path could work I'm wondering if a cleaner solution would be to utilize |
The paths should still be canonicalized appropriately. A combination would be best. I forsee no immediate issues with using |
bec265a
to
ff15c7c
Compare
|
Hopefully that fixes the issue properly now. I wasn't able to verify with a non malicious zip because I couldn't figure out how to make one that passed |
|
Also I'm not 100% sold on the design of my code but wasn't sure how I would make it better so feel free to rip that apart :) |
You'll need to create the ZIP on the host then copy it to the filesystem. Every directory in |
Thats good to know! I've been using a simple http server to download things with |
What does "complained" mean ? I suggest that the complaint is a result of your changes to |
|
It happens on a clean master build too. @bcoles are you in the IRC channel because I could ping you there :) Edit: Worked out the issue on IRC, I didn't read the error I was getting |
|
For reference, this was discussed on IRC and the issue with decompression was resolved (and was unrelated to the patches in this PR). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a look at the unzip patch only. My commentary may also apply to tar.
Userland/Utilities/unzip.cpp
Outdated
| fprintf(stderr, "PATH unveil\n"); | ||
| perror("unveil"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using both fprintf and perror seems redundant. Simply perror("unveil") is sufficient.
Userland/Utilities/unzip.cpp
Outdated
| StringBuilder target_path; | ||
| // Currently unzip doesn't support extracting to a specified location. If that is implemented the target_path | ||
| // should be replaced with the target path from the command line arguements instead of "." | ||
| target_path.append(realpath(".", nullptr)); | ||
| target_path.append("/"); | ||
| auto target_path_string = target_path.to_string(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The target_path is effectively the same as cwd which is defined and unveiled earlier. target_path is a more future-proof name and it would make sense to group these together and use the same variable.
This way, if someone adds support for decompressing to a user-specified directory (ie, unzip -d /output/path) in the future, they only need to change one line as there's no hard coded assumption of operating in the working directory.
Userland/Utilities/unzip.cpp
Outdated
| fprintf(stderr, "CWD unveil\n"); | ||
| perror("unveil"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using both fprintf and perror seems redundant. Simply perror("unveil") is sufficient.
|
I tried the patched An archive containing On the other hand, On the other other hand, perhaps this is outside the scope of this PR. |
|
I think its in scope. The issue you saw was from some sloppy programming on my part. The issue happens because I use the following check to determine if a file is in the target path The reason you see this error is because for |
It is a realistic thing to worry about, but you should be able to use the canonicalized path for the |
This change makes LexicalPath objects directory aware. This allows programs to resolve directory paths that contain special characters without having to juggle appending a '/'. This doesn't matter a ton but it helps when coding things that are checking paths.
ff15c7c
to
2063dd9
Compare
|
Hopefully I'm getting close with this now. I did make an ease of use change to LexicalPath. If that isn't an acceptable change I would love to hear some ideas on how to handle archive file paths that share prefixes with the target path but are not the same directory. For example
|
This change validates the filenames within a tar/zip archive during extraction. If the filename within a the archive is outside of the current working directory the file will be skipped and not extracted onto the host system. Closes SerenityOS#3991 Closes SerenityOS#3992
2063dd9
to
3844e85
Compare
|
@bblenard there are two merge conflicts, please rebase this PR :) |
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions! |
|
This pull request has been closed because it has not had recent activity. Feel free to re-open if you wish to still contribute these changes. Thank you for your contributions! |








This change validates the filenames within a tar/zip archive during
extraction. If the filename within a the archive is outside of the
current working directory the file will be skipped and not extracted
onto the host system.
Closes #3991
Closes #3992