Skip to content

Commit f5ab743

Browse files
committed
fix: Prevent download of view-only files
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent 513c0cc commit f5ab743

4 files changed

Lines changed: 65 additions & 36 deletions

File tree

apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,14 @@ public function handleDownload(Request $request, Response $response): ?bool {
137137
}
138138

139139
$folder = $node->getNode();
140-
$event = new BeforeZipCreatedEvent($folder, $files);
140+
$nodes = empty($files) ? $folder->getDirectoryListing() : [];
141+
foreach ($files as $path) {
142+
$child = $node->getChild($path);
143+
assert($child instanceof Node);
144+
$nodes[] = $child->getNode();
145+
}
146+
147+
$event = new BeforeZipCreatedEvent($folder, $files, $nodes);
141148
$this->eventDispatcher->dispatchTyped($event);
142149
if ((!$event->isSuccessful()) || $event->getErrorMessage() !== null) {
143150
$errorMessage = $event->getErrorMessage();
@@ -150,12 +157,7 @@ public function handleDownload(Request $request, Response $response): ?bool {
150157
throw new Forbidden($errorMessage);
151158
}
152159

153-
$content = empty($files) ? $folder->getDirectoryListing() : [];
154-
foreach ($files as $path) {
155-
$child = $node->getChild($path);
156-
assert($child instanceof Node);
157-
$content[] = $child->getNode();
158-
}
160+
$nodes = $event->getNodes();
159161

160162
$archiveName = $folder->getName();
161163
if (count(explode('/', trim($folder->getPath(), '/'), 3)) === 2) {
@@ -169,13 +171,13 @@ public function handleDownload(Request $request, Response $response): ?bool {
169171
$rootPath = dirname($folder->getPath());
170172
}
171173

172-
$streamer = new Streamer($tarRequest, -1, count($content), $this->timezoneFactory);
174+
$streamer = new Streamer($tarRequest, -1, count($nodes), $this->timezoneFactory);
173175
$streamer->sendHeaders($archiveName);
174176
// For full folder downloads we also add the folder itself to the archive
175177
if (empty($files)) {
176178
$streamer->addEmptyDir($archiveName);
177179
}
178-
foreach ($content as $node) {
180+
foreach ($nodes as $node) {
179181
$this->streamNode($streamer, $node, $rootPath);
180182
}
181183
$streamer->finalize();

apps/files_sharing/lib/Listener/BeforeZipCreatedListener.php

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,38 @@ public function handle(Event $event): void {
3232
return;
3333
}
3434

35-
$dir = $event->getDirectory();
36-
$files = $event->getFiles();
35+
$user = $this->userSession->getUser();
36+
if (!$user) {
37+
// fixme: do we need check anything for anonymous users?
38+
$event->setSuccessful(true);
39+
}
3740

38-
$pathsToCheck = [];
39-
foreach ($files as $file) {
41+
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
42+
$viewOnlyHandler = new ViewOnly($userFolder);
43+
$this->checkSelectedFilesCanBeDownloaded($event, $viewOnlyHandler);
44+
$this->checkNodesCanBeDownloaded($event, $viewOnlyHandler);
45+
}
46+
47+
private function checkSelectedFilesCanBeDownloaded(BeforeZipCreatedEvent $event, ViewOnly $viewOnlyHandler): void {
48+
// Check only for user/group shares. Don't restrict e.g. share links
49+
$dir = $event->getDirectory();
50+
$pathsToCheck = [$dir];
51+
foreach ($event->getFiles() as $file) {
4052
$pathsToCheck[] = $dir . '/' . $file;
4153
}
4254

43-
// Check only for user/group shares. Don't restrict e.g. share links
44-
$user = $this->userSession->getUser();
45-
if ($user) {
46-
$viewOnlyHandler = new ViewOnly(
47-
$this->rootFolder->getUserFolder($user->getUID())
48-
);
49-
if (!$viewOnlyHandler->check($pathsToCheck)) {
50-
$event->setErrorMessage('Access to this resource or one of its sub-items has been denied.');
51-
$event->setSuccessful(false);
52-
} else {
53-
$event->setSuccessful(true);
54-
}
55+
if (!$viewOnlyHandler->check($pathsToCheck)) {
56+
$event->setErrorMessage('Access to this resource or one of its sub-items has been denied.');
57+
$event->setSuccessful(false);
5558
} else {
5659
$event->setSuccessful(true);
5760
}
5861
}
62+
63+
private function checkNodesCanBeDownloaded(BeforeZipCreatedEvent $event, ViewOnly $viewOnlyHandler): void {
64+
$nodes = array_values(array_filter($event->getNodes(),
65+
static fn ($node) => $viewOnlyHandler->checkNode($node)));
66+
67+
$event->setNodes($nodes);
68+
}
5969
}

apps/files_sharing/lib/ViewOnly.php

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,17 @@ public function __construct(
2323
) {
2424
}
2525

26+
public function checkNode(Node $node): bool {
27+
return match (true) {
28+
// access to filecache is expensive in the loop
29+
$node instanceof File => $this->checkFileInfo($node),
30+
// get directory content is a rather cheap query
31+
$node instanceof Folder => $this->dirRecursiveCheck($node),
32+
};
33+
}
34+
2635
/**
36+
* // fixme: inline this method to listener
2737
* @param string[] $pathsToCheck
2838
* @return bool
2939
*/
@@ -32,17 +42,7 @@ public function check(array $pathsToCheck): bool {
3242
foreach ($pathsToCheck as $file) {
3343
try {
3444
$info = $this->userFolder->get($file);
35-
if ($info instanceof File) {
36-
// access to filecache is expensive in the loop
37-
if (!$this->checkFileInfo($info)) {
38-
return false;
39-
}
40-
} elseif ($info instanceof Folder) {
41-
// get directory content is rather cheap query
42-
if (!$this->dirRecursiveCheck($info)) {
43-
return false;
44-
}
45-
}
45+
return $this->checkNode($info);
4646
} catch (NotFoundException $e) {
4747
continue;
4848
}

lib/public/Files/Events/BeforeZipCreatedEvent.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace OCP\Files\Events;
1111

12+
use OCP\Files\Node;
1213
use OCP\EventDispatcher\Event;
1314
use OCP\Files\Folder;
1415

@@ -28,12 +29,14 @@ class BeforeZipCreatedEvent extends Event {
2829

2930
/**
3031
* @param list<string> $files
32+
* @param list<Node> $nodes
3133
* @since 25.0.0
3234
* @since 31.0.0 support `OCP\Files\Folder` as `$directory` parameter - passing a string is deprecated now
3335
*/
3436
public function __construct(
3537
string|Folder $directory,
3638
private array $files,
39+
private array $nodes = [],
3740
) {
3841
parent::__construct();
3942
if ($directory instanceof Folder) {
@@ -65,6 +68,20 @@ public function getFiles(): array {
6568
return $this->files;
6669
}
6770

71+
/**
72+
* @return Node[]
73+
*/
74+
public function getNodes(): array {
75+
return $this->nodes;
76+
}
77+
78+
/**
79+
* @param Node[] $nodes
80+
*/
81+
public function setNodes(array $nodes): void {
82+
$this->nodes = $nodes;
83+
}
84+
6885
/**
6986
* @since 25.0.0
7087
*/

0 commit comments

Comments
 (0)