phar: reject extractTo() when destination parent escapes via symlink#21808
phar: reject extractTo() when destination parent escapes via symlink#21808iliaal wants to merge 1 commit intophp:PHP-8.4from
Conversation
| return FAILURE; | ||
| } | ||
| size_t resolved_dest_len = strlen(resolved_dest); | ||
| if (strncmp(resolved, resolved_dest, resolved_dest_len) != 0 || |
There was a problem hiding this comment.
nit: could use zend_binary_strncmp
There was a problem hiding this comment.
Keeping strncmp. phar uses it throughout for path prefix checks (util.c:1296, stream.c:615, phar.c:509). zend_binary_strncmp carries three-way-compare length semantics for userland zend_string comparison, not a prefix check on NUL-terminated realpath output.
| } | ||
| size_t resolved_dest_len = strlen(resolved_dest); | ||
| if (strncmp(resolved, resolved_dest, resolved_dest_len) != 0 || | ||
| (resolved[resolved_dest_len] != '/' && resolved[resolved_dest_len] != '\\' && resolved[resolved_dest_len] != '\0')) { |
There was a problem hiding this comment.
nit: could use IS_SLASH
There was a problem hiding this comment.
Good point, updated.
5029b6a to
e2a8b21
Compare
Girgias
left a comment
There was a problem hiding this comment.
Frankly struggling to understand the fix. Comments are useful to describe what the purpose of the code and why it is being done.
| return SUCCESS; | ||
| } | ||
|
|
||
| { |
There was a problem hiding this comment.
Why? Why are you creating a block scope, we use C99 (heck even C11...) you can just declare a new variable.
| } | ||
|
|
||
| { | ||
| const char *last_sep = zend_memrchr(fullpath + dest_len + 1, '/', strlen(fullpath) - dest_len - 1); |
There was a problem hiding this comment.
We have the length of fullpath already.
| const char *last_sep = zend_memrchr(fullpath + dest_len + 1, '/', strlen(fullpath) - dest_len - 1); | |
| const char *last_sep = zend_memrchr(fullpath + dest_len + 1, '/', len - dest_len - 1); |
| if (parent_len >= MAXPATHLEN) { | ||
| spprintf(error, 4096, "Cannot extract \"%s\", path too long", entry->filename); | ||
| efree(fullpath); | ||
| return FAILURE; | ||
| } |
There was a problem hiding this comment.
How would this ever happen? This is derived from fullpath and this was already checked to fit within the maxpathlen.
Convert this to an assertion.
There was a problem hiding this comment.
Yup, you right, fixed.
Phar::extractTo() builds the target path as dest + "/" + entry_path. CWD_EXPAND validates entry_path against ".." traversal but doesn't resolve symlinks. A pre-existing symlink in the destination (e.g., /dest/subdir -> /etc) writes the extracted file outside the intended root. After creating the parent directory, resolve it with VCWD_REALPATH and verify it stays under dest. Return FAILURE if it escapes so the caller throws a PharException.
e2a8b21 to
0ff1ded
Compare
|
Addressed the comments: dropped the block scope, reused For the overall approach: The patch resolves the parent directory of the target with |
Fixes destination-side symlink traversal in
Phar::extractTo().extractTo()builds the target path asdest + "/" + entry_path.CWD_EXPANDvalidates the entry path against..traversal but doesn't resolve symlinks. A pre-existing symlink in the destination (e.g.,/dest/subdir -> /etc) causes the extracted file to land outside the intended root.After creating the parent directory, resolve it with
VCWD_REALPATHand verify it stays underdest. ReturnFAILUREif it escapes so the caller throws aPharException.The test skips on Windows only when
SeCreateSymbolicLinkPrivilegeis absent. Without it,symlink()returns false without error, so no symlink exists in the destination,extractTo()succeeds, and the test outputsEXTRACTED (unexpected)instead of thePharExceptionthat--EXPECTF--requires.