Skip to content

Commit 17b40e1

Browse files
committed
fix: Prevent download of view-only files (alternative approach)
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent fc61b22 commit 17b40e1

File tree

4 files changed

+54
-134
lines changed

4 files changed

+54
-134
lines changed

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,21 @@ public function initialize(Server $server): void {
6161
$this->server->on('afterMethod:GET', $this->afterDownload(...), 999);
6262
}
6363

64+
/**
65+
* Recursively iterate over all nodes in a folder.
66+
*/
67+
protected function iterateNodes(NcNode $node): iterable {
68+
if ($node instanceof NcFile) {
69+
yield $node;
70+
} elseif ($node instanceof NcFolder) {
71+
foreach ($node->getDirectoryListing() as $childNode) {
72+
yield from $this->iterateNodes($childNode);
73+
}
74+
}
75+
}
76+
6477
/**
6578
* Adding a node to the archive streamer.
66-
* This will recursively add new nodes to the stream if the node is a directory.
6779
*/
6880
protected function streamNode(Streamer $streamer, NcNode $node, string $rootPath): void {
6981
// Remove the root path from the filename to make it relative to the requested folder
@@ -79,10 +91,6 @@ protected function streamNode(Streamer $streamer, NcNode $node, string $rootPath
7991
$streamer->addFileFromStream($resource, $filename, $node->getSize(), $mtime);
8092
} elseif ($node instanceof NcFolder) {
8193
$streamer->addEmptyDir($filename, $mtime);
82-
$content = $node->getDirectoryListing();
83-
foreach ($content as $subNode) {
84-
$this->streamNode($streamer, $subNode, $rootPath);
85-
}
8694
}
8795
}
8896

@@ -137,14 +145,20 @@ public function handleDownload(Request $request, Response $response): ?bool {
137145
}
138146

139147
$folder = $node->getNode();
140-
$nodes = empty($files) ? $folder->getDirectoryListing() : [];
148+
$rootNodes = empty($files) ? $folder->getDirectoryListing() : [];
141149
foreach ($files as $path) {
142150
$child = $node->getChild($path);
143151
assert($child instanceof Node);
144-
$nodes[] = $child->getNode();
152+
$rootNodes[] = $child->getNode();
153+
}
154+
$allNodes = [];
155+
foreach ($rootNodes as $rootNode) {
156+
foreach ($this->iterateNodes($rootNode) as $node) {
157+
$allNodes[] = $node;
158+
}
145159
}
146160

147-
$event = new BeforeZipCreatedEvent($folder, $files, $nodes);
161+
$event = new BeforeZipCreatedEvent($folder, $files, $allNodes);
148162
$this->eventDispatcher->dispatchTyped($event);
149163
if ((!$event->isSuccessful()) || $event->getErrorMessage() !== null) {
150164
$errorMessage = $event->getErrorMessage();
@@ -156,8 +170,7 @@ public function handleDownload(Request $request, Response $response): ?bool {
156170
// Downloading was denied by an app
157171
throw new Forbidden($errorMessage);
158172
}
159-
160-
$nodes = $event->getNodes();
173+
$allNodes = $event->getNodes();
161174

162175
$archiveName = $folder->getName();
163176
if (count(explode('/', trim($folder->getPath(), '/'), 3)) === 2) {
@@ -171,13 +184,13 @@ public function handleDownload(Request $request, Response $response): ?bool {
171184
$rootPath = dirname($folder->getPath());
172185
}
173186

174-
$streamer = new Streamer($tarRequest, -1, count($nodes), $this->timezoneFactory);
187+
$streamer = new Streamer($tarRequest, -1, count($rootNodes), $this->timezoneFactory);
175188
$streamer->sendHeaders($archiveName);
176189
// For full folder downloads we also add the folder itself to the archive
177190
if (empty($files)) {
178191
$streamer->addEmptyDir($archiveName);
179192
}
180-
foreach ($nodes as $node) {
193+
foreach ($allNodes as $node) {
181194
$this->streamNode($streamer, $node, $rootPath);
182195
}
183196
$streamer->finalize();

apps/files_sharing/lib/Listener/BeforeZipCreatedListener.php

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99

1010
namespace OCA\Files_Sharing\Listener;
1111

12-
use OCA\Files_Sharing\ViewOnly;
12+
use OCA\Files_Sharing\SharedStorage;
1313
use OCP\EventDispatcher\Event;
1414
use OCP\EventDispatcher\IEventListener;
1515
use OCP\Files\Events\BeforeZipCreatedEvent;
1616
use OCP\Files\IRootFolder;
17+
use OCP\Files\Node;
1718
use OCP\IUserSession;
1819

1920
/**
@@ -36,34 +37,39 @@ public function handle(Event $event): void {
3637
if (!$user) {
3738
// fixme: do we need check anything for anonymous users?
3839
$event->setSuccessful(true);
40+
41+
return;
3942
}
4043

4144
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
42-
$viewOnlyHandler = new ViewOnly($userFolder);
43-
$this->checkSelectedFilesCanBeDownloaded($event, $viewOnlyHandler);
44-
$this->checkNodesCanBeDownloaded($event, $viewOnlyHandler);
45+
// Check whether the user can download requested folder
46+
$folder = $userFolder->get(substr($event->getDirectory(), strlen($userFolder->getPath())));
47+
if (!$this->isNodeCanBeDownloaded($folder)) {
48+
$event->setSuccessful(false);
49+
$event->setErrorMessage('Access to this resource has been denied.');
50+
return;
51+
}
52+
53+
$nodes = array_filter($event->getNodes(), fn($node) => $this->isNodeCanBeDownloaded($node));
54+
$event->setNodes(array_values($nodes));
55+
$event->setSuccessful(true);
4556
}
4657

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) {
52-
$pathsToCheck[] = $dir . '/' . $file;
58+
public function isNodeCanBeDownloaded(Node $node): bool {
59+
// Restrict view-only to nodes which are shared
60+
$storage = $node->getStorage();
61+
if (!$storage->instanceOfStorage(SharedStorage::class)) {
62+
return true;
5363
}
5464

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);
58-
} else {
59-
$event->setSuccessful(true);
60-
}
61-
}
65+
// Extract extra permissions
66+
/** @var SharedStorage $storage */
67+
$share = $storage->getShare();
6268

63-
private function checkNodesCanBeDownloaded(BeforeZipCreatedEvent $event, ViewOnly $viewOnlyHandler): void {
64-
$nodes = array_values(array_filter($event->getNodes(),
65-
static fn ($node) => $viewOnlyHandler->checkNode($node)));
69+
// Check whether download-permission was denied (granted if not set)
70+
$attributes = $share->getAttributes();
71+
$canDownload = $attributes?->getAttribute('permissions', 'download');
6672

67-
$event->setNodes($nodes);
73+
return $canDownload !== false;
6874
}
6975
}

apps/files_sharing/lib/ViewOnly.php

Lines changed: 0 additions & 99 deletions
This file was deleted.

lib/public/Files/Events/BeforeZipCreatedEvent.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ class BeforeZipCreatedEvent extends Event {
2828
private ?Folder $folder = null;
2929

3030
/**
31-
* @param list<string> $files
32-
* @param list<Node> $nodes
31+
* @param list<string> $files Selected files, empty for folder selection
32+
* @param list<Node> $nodes Recursively collected nodes
3333
* @since 25.0.0
3434
* @since 31.0.0 support `OCP\Files\Folder` as `$directory` parameter - passing a string is deprecated now
3535
*/

0 commit comments

Comments
 (0)