Skip to content

Commit b17fea7

Browse files
committed
fix: Prevent download of view-only files (optimized approach using generators)
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent 5ebb385 commit b17fea7

5 files changed

Lines changed: 42 additions & 41 deletions

File tree

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

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

64+
/**
65+
* @return iterable<NcNode>
66+
*/
67+
protected function createIterator(array $rootNodes): iterable {
68+
foreach ($rootNodes as $rootNode) {
69+
yield from $this->iterateNodes($rootNode);
70+
}
71+
}
72+
6473
/**
6574
* Recursively iterate over all nodes in a folder.
75+
* @return iterable<NcNode>
6676
*/
6777
protected function iterateNodes(NcNode $node): iterable {
6878
yield $node;
@@ -151,14 +161,8 @@ public function handleDownload(Request $request, Response $response): ?bool {
151161
assert($child instanceof Node);
152162
$rootNodes[] = $child->getNode();
153163
}
154-
$allNodes = [];
155-
foreach ($rootNodes as $rootNode) {
156-
foreach ($this->iterateNodes($rootNode) as $node) {
157-
$allNodes[] = $node;
158-
}
159-
}
160164

161-
$event = new BeforeZipCreatedEvent($folder, $files, $allNodes);
165+
$event = new BeforeZipCreatedEvent($folder, $files, $this->createIterator($rootNodes));
162166
$this->eventDispatcher->dispatchTyped($event);
163167
if ((!$event->isSuccessful()) || $event->getErrorMessage() !== null) {
164168
$errorMessage = $event->getErrorMessage();
@@ -170,7 +174,6 @@ public function handleDownload(Request $request, Response $response): ?bool {
170174
// Downloading was denied by an app
171175
throw new Forbidden($errorMessage);
172176
}
173-
$allNodes = $event->getNodes();
174177

175178
$archiveName = $folder->getName();
176179
if (count(explode('/', trim($folder->getPath(), '/'), 3)) === 2) {
@@ -190,7 +193,7 @@ public function handleDownload(Request $request, Response $response): ?bool {
190193
if (empty($files)) {
191194
$streamer->addEmptyDir($archiveName);
192195
}
193-
foreach ($allNodes as $node) {
196+
foreach ($event->getNodes() as $node) {
194197
$this->streamNode($streamer, $node, $rootPath);
195198
}
196199
$streamer->finalize();

apps/files/lib/Controller/ConversionApiController.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@
2121
use OCP\AppFramework\OCS\OCSForbiddenException;
2222
use OCP\AppFramework\OCS\OCSNotFoundException;
2323
use OCP\AppFramework\OCSController;
24-
use OCP\EventDispatcher\IEventDispatcher;
2524
use OCP\Files\Conversion\IConversionManager;
26-
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
2725
use OCP\Files\File;
2826
use OCP\Files\GenericFileException;
2927
use OCP\Files\IRootFolder;
@@ -39,7 +37,6 @@ public function __construct(
3937
private IRootFolder $rootFolder,
4038
private IL10N $l10n,
4139
private ?string $userId,
42-
private IEventDispatcher $eventDispatcher,
4340
) {
4441
parent::__construct($appName, $request);
4542
}
@@ -70,13 +67,6 @@ public function convert(int $fileId, string $targetMimeType, ?string $destinatio
7067
throw new OCSNotFoundException($this->l10n->t('The file cannot be found'));
7168
}
7269

73-
$event = new BeforeDirectFileDownloadEvent($userFolder->getRelativePath($file->getPath()));
74-
$this->eventDispatcher->dispatchTyped($event);
75-
76-
if ($event->isSuccessful() === false) {
77-
throw new OCSForbiddenException('Permission denied to download file');
78-
}
79-
8070
if ($destination !== null) {
8171
$destination = PathHelper::normalizePath($destination);
8272
$parentDir = dirname($destination);

apps/files_sharing/lib/Listener/BeforeZipCreatedListener.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
use OCP\EventDispatcher\Event;
1414
use OCP\EventDispatcher\IEventListener;
1515
use OCP\Files\Events\BeforeZipCreatedEvent;
16-
use OCP\Files\IRootFolder;
16+
use OCP\Files\Node;
1717
use OCP\IUserSession;
1818

1919
/**
@@ -23,7 +23,6 @@ class BeforeZipCreatedListener implements IEventListener {
2323

2424
public function __construct(
2525
private IUserSession $userSession,
26-
private IRootFolder $rootFolder,
2726
private ViewOnly $viewOnly,
2827
) {
2928
}
@@ -38,17 +37,14 @@ public function handle(Event $event): void {
3837
return;
3938
}
4039

41-
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
4240
// Check whether the user can download the requested folder
43-
$folder = $userFolder->get(substr($event->getDirectory(), strlen($userFolder->getPath())));
44-
if (!$this->viewOnly->isNodeCanBeDownloaded($folder)) {
41+
if (!$this->viewOnly->isNodeCanBeDownloaded($event->getFolder())) {
4542
$event->setSuccessful(false);
4643
$event->setErrorMessage('Access to this resource has been denied.');
4744
return;
4845
}
4946

50-
$nodes = array_filter($event->getNodes(), fn ($node) => $this->viewOnly->isNodeCanBeDownloaded($node));
51-
$event->setNodes(array_values($nodes));
52-
$event->setSuccessful(true);
47+
// Check recursively whether the user can download nested nodes
48+
$event->addNodeFilter(fn (Node $node) => $this->viewOnly->isNodeCanBeDownloaded($node));
5349
}
5450
}

apps/files_sharing/tests/ApplicationTest.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use OCA\Files_Sharing\Listener\BeforeDirectFileDownloadListener;
1414
use OCA\Files_Sharing\Listener\BeforeZipCreatedListener;
1515
use OCA\Files_Sharing\SharedStorage;
16+
use OCA\Files_Sharing\ViewOnly;
1617
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
1718
use OCP\Files\Events\BeforeZipCreatedEvent;
1819
use OCP\Files\File;
@@ -91,7 +92,8 @@ public function testCheckDirectCanBeDownloaded(
9192
$event = new BeforeDirectFileDownloadEvent($path);
9293
$listener = new BeforeDirectFileDownloadListener(
9394
$this->userSession,
94-
$this->rootFolder
95+
$this->rootFolder,
96+
new ViewOnly(),
9597
);
9698
$listener->handle($event);
9799

@@ -177,10 +179,10 @@ function (string $fileStorage) use ($nonSharedStorage, $secureSharedStorage) {
177179
$this->rootFolder->method('getUserFolder')->with('test')->willReturn($userFolder);
178180

179181
// Simulate zip download of folder folder
180-
$event = new BeforeZipCreatedEvent($dir, $files);
182+
$event = new BeforeZipCreatedEvent($dir, $files, []);
181183
$listener = new BeforeZipCreatedListener(
182184
$this->userSession,
183-
$this->rootFolder
185+
new ViewOnly(),
184186
);
185187
$listener->handle($event);
186188

@@ -192,10 +194,10 @@ public function testCheckFileUserNotFound(): void {
192194
$this->userSession->method('isLoggedIn')->willReturn(false);
193195

194196
// Simulate zip download of folder folder
195-
$event = new BeforeZipCreatedEvent('/test', ['test.txt']);
197+
$event = new BeforeZipCreatedEvent('/test', ['test.txt'], []);
196198
$listener = new BeforeZipCreatedListener(
197199
$this->userSession,
198-
$this->rootFolder
200+
new ViewOnly(),
199201
);
200202
$listener->handle($event);
201203

lib/public/Files/Events/BeforeZipCreatedEvent.php

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,19 @@ class BeforeZipCreatedEvent extends Event {
2626
private bool $successful = true;
2727
private ?string $errorMessage = null;
2828
private ?Folder $folder = null;
29+
private array $nodeFilters = [];
2930

3031
/**
3132
* @param string|Folder $directory Folder instance, or (deprecated) string path relative to user folder
32-
* @param list<string> $files
3333
* @param list<string> $files Selected files, empty for folder selection
34-
* @param list<Node> $nodes Recursively collected nodes
34+
* @param iterable<Node> $nodes Recursively collected nodes
3535
* @since 25.0.0
3636
* @since 31.0.0 support `OCP\Files\Folder` as `$directory` parameter - passing a string is deprecated now
3737
*/
3838
public function __construct(
3939
string|Folder $directory,
4040
private array $files,
41-
private array $nodes = [],
41+
private iterable $nodes,
4242
) {
4343
parent::__construct();
4444
if ($directory instanceof Folder) {
@@ -75,17 +75,27 @@ public function getFiles(): array {
7575
}
7676

7777
/**
78-
* @return Node[]
78+
* @return iterable<Node>
7979
*/
80-
public function getNodes(): array {
81-
return $this->nodes;
80+
public function getNodes(): iterable {
81+
foreach ($this->nodes as $node) {
82+
$pass = true;
83+
foreach ($this->nodeFilters as $filter) {
84+
$pass = $pass && $filter($node);
85+
}
86+
87+
if ($pass) {
88+
yield $node;
89+
}
90+
}
8291
}
8392

8493
/**
85-
* @param Node[] $nodes
94+
* @param callable $filter
95+
* @return void
8696
*/
87-
public function setNodes(array $nodes): void {
88-
$this->nodes = $nodes;
97+
public function addNodeFilter(callable $filter): void {
98+
$this->nodeFilters[] = $filter;
8999
}
90100

91101
/**

0 commit comments

Comments
 (0)