Harden archive extraction: zip-slip protection + exit-code error handling#147
Open
kaibae19 wants to merge 2 commits into
Open
Harden archive extraction: zip-slip protection + exit-code error handling#147kaibae19 wants to merge 2 commits into
kaibae19 wants to merge 2 commits into
Conversation
ZipArchive::extractTo is safe on modern libzip, but the unrar and 7za codepaths blindly trust archive entry paths and will write outside $extractTo when an entry begins with "/" or contains a ".." path component. A user uploading a crafted archive can therefore write into directories the nc-app process can reach. Add a per-format pre-extraction listing pass (ZipArchive indices for ZIP, `unrar lb` for RAR, `7za l -ba -slt` for the rest) and reject the extraction up front if any entry would escape the destination. The existing extraction commands and exit-code handling are unchanged. Signed-off-by: kaibae19 <99116238+kaibae19@users.noreply.github.com>
extractRar and extractOther were inferring success from `sizeof($output) <= 4` / `<= 5` — a brittle heuristic. A successful extract of a small archive (few stdout lines) can be misreported as a failure, and a partial/silent failure with verbose stdout can be misreported as a success. Capture and check the real exit code from exec(), log the full tool output on failure, and replace the "Oops" copy with a more actionable error mentioning the missing tool. Also redirect stderr into $output so error details from unrar / 7za actually reach the log. Signed-off-by: kaibae19 <99116238+kaibae19@users.noreply.github.com>
Author
Security review of
|
| Check | Result |
|---|---|
PHP sink grep (exec / eval / include $var / preg_replace /e / create_function / etc.) |
Only the four escapeshellarg-wrapped exec() calls in ExtractionService.php (two new for listing, two existing for extraction). Nothing else. |
Patched ExtractionService.php shell safety |
$file and $extractTo are server-resolved local-FS paths (IRootFolder → getLocalFile), wrapped with escapeshellarg. No user-controlled string reaches the shell unquoted. |
JS XSS sink grep (v-html / innerHTML / outerHTML / dangerouslySetInnerHTML / eval / Function( / document.write) |
None. |
ExtractionController.php authn / authz |
#[NoAdminRequired] is correct for a per-user operation. Path resolution goes through $this->userFolder = $this->rootFolder->getUserFolder($this->userId) — a user can only target their own files. |
CSRF posture of /api/v1/extraction/execute |
Inherited via OCSController → built-in OCS-APIRequest header / CSRF-token requirement. No #[NoCSRFRequired]. Safe. |
Application.php, LoadExtractActions.php |
Only Util::addScript / Util::addStyle. No attack surface. |
Capabilities.php |
Static feature list plus appManager->getAppVersion(). No user input. |
extract-action.ts (frontend) |
Builds the POST payload from Node.attributes (already validated by NC), posts via @nextcloud/axios (auto CSRF). On success constructs a Folder from the response; every field consumed (fileId, source, root, owner, permissions, mtime, mount-type, owner-display-name) is server-generated. No DOM injection. |
templates/ |
content/index.php is <h1>Hello world</h1>; settings/index.php is an empty scaffold. Cosmetic dead code — flagged for cleanup but no security concern. |
composer audit |
No security vulnerability advisories found (28 packages). |
npm audit --omit=dev --audit-level=high |
0 vulnerabilities (the audit-fix was applied during the local build before pushing). |
psalm against the project's psalm.xml + baseline |
0 errors. The two commits introduce no static-analysis regressions. |
Verdict
No XSS, SSRF, CSRF, command-injection, or path-traversal findings in the diff or the surrounding code. The two commits are safe to land as-is from a security standpoint.
Non-security observations surfaced during the review (not blocking, FYI)
src/actions/extract-action.ts:73—window.OCP.Files.Router.goToRoute({..., fileid: data.fileId }, ...)readsdata.fileId, but the server returns it underdata.ocs.data.extracted.fileId. Looks like a navigation bug (post-extract redirect probably no-ops), not security. Out of scope here.templates/content/index.php— literal<h1>Hello world</h1>. Cleanup target.- Pre-existing items worth tracking separately: no decompression-bomb / archive-size cap;
ExtractionController.phpimports privateOC\Files\FilesystemforisFileBlacklisted, which may break across NC minor bumps.
Happy to file the non-security observations as separate issues if useful.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two small, independent fixes to
ExtractionService.php:Reject archive entries whose paths would escape the destination. Modern libzip already blocks traversal in
ZipArchive::extractTo, but theunrarand7zacodepaths blindly trust archive entry paths. A crafted archive with an entry named e.g.../../etc/somethingwould write outside$extractTo. The first commit adds a per-format pre-extraction listing pass (libzip indices for ZIP,unrar lbfor RAR,7za l -ba -sltfor the rest) and refuses to extract anything if an entry has a leading/or any..path component.Use real exit codes to detect tool failure instead of
sizeof($output) <= 4/<= 5. The current heuristic can both miss real failures (verbose error output makes a failed extract look successful) and produce false positives (a successful extract of a small archive looks like a failure). The second commit checks$return, captures stderr into the output, and logs the full tool output on failure.The commits are independent and can be cherry-picked separately.
Security review
A full security review of this branch (codebase walk,
composer audit,npm audit, psalm, and a targeted look at the patched diff) came back clean — no XSS / SSRF / CSRF / command-injection findings and no static-analysis or dependency-audit regressions. Full report in the first comment on this PR.Test plan
../escape.txtis refused with "Archive contains an unsafe path..." (verified locally via the OCSexecuteroute).../escape.txt(extractOther codepath via7za) is refused.sizeof <= Nfalse-failure path.Notes
unrar lband7za l -ba -sltare part of the sameunrar/p7zip-fullpackages the existing extraction commands already require.unrar lbcall assumes proprietaryunrar(matching the existingunrar x ... -R ... -o+syntax). If you'd like the RAR path to also work onunrar-free, happy to add a second listing strategy.