Skip to content

Commit 1124eaa

Browse files
Merge pull request #50442 from nextcloud/backport/50430/stable31
[stable31] files: harden thumbnail endpoint
2 parents e5d4bdd + 76ac30f commit 1124eaa

File tree

2 files changed

+102
-6
lines changed

2 files changed

+102
-6
lines changed

apps/files/lib/Controller/ApiController.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@
2929
use OCP\AppFramework\Http\StreamResponse;
3030
use OCP\Files\File;
3131
use OCP\Files\Folder;
32+
use OCP\Files\InvalidPathException;
3233
use OCP\Files\IRootFolder;
3334
use OCP\Files\NotFoundException;
35+
use OCP\Files\NotPermittedException;
36+
use OCP\Files\Storage\ISharedStorage;
3437
use OCP\Files\StorageNotAvailableException;
3538
use OCP\IConfig;
3639
use OCP\IL10N;
@@ -92,20 +95,28 @@ public function getThumbnail($x, $y, $file) {
9295
}
9396

9497
try {
95-
$file = $this->userFolder->get($file);
96-
if ($file instanceof Folder) {
98+
$file = $this->userFolder?->get($file);
99+
if ($file === null
100+
|| !($file instanceof File)
101+
|| ($file->getId() <= 0)
102+
) {
97103
throw new NotFoundException();
98104
}
99105

100-
if ($file->getId() <= 0) {
101-
return new DataResponse(['message' => 'File not found.'], Http::STATUS_NOT_FOUND);
106+
// Validate the user is allowed to download the file (preview is some kind of download)
107+
$storage = $file->getStorage();
108+
if ($storage->instanceOfStorage(ISharedStorage::class)) {
109+
/** @var ISharedStorage $storage */
110+
$attributes = $storage->getShare()->getAttributes();
111+
if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) {
112+
throw new NotFoundException();
113+
}
102114
}
103115

104-
/** @var File $file */
105116
$preview = $this->previewManager->getPreview($file, $x, $y, true);
106117

107118
return new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]);
108-
} catch (NotFoundException $e) {
119+
} catch (NotFoundException|NotPermittedException|InvalidPathException) {
109120
return new DataResponse(['message' => 'File not found.'], Http::STATUS_NOT_FOUND);
110121
} catch (\Exception $e) {
111122
return new DataResponse([], Http::STATUS_BAD_REQUEST);

apps/files/tests/Controller/ApiControllerTest.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@
1919
use OCP\Files\IRootFolder;
2020
use OCP\Files\NotFoundException;
2121
use OCP\Files\SimpleFS\ISimpleFile;
22+
use OCP\Files\Storage\ISharedStorage;
23+
use OCP\Files\Storage\IStorage;
2224
use OCP\Files\StorageNotAvailableException;
2325
use OCP\IConfig;
2426
use OCP\IL10N;
2527
use OCP\IPreview;
2628
use OCP\IRequest;
2729
use OCP\IUser;
2830
use OCP\IUserSession;
31+
use OCP\Share\IAttributes;
2932
use OCP\Share\IManager;
33+
use OCP\Share\IShare;
3034
use Psr\Log\LoggerInterface;
3135
use Test\TestCase;
3236

@@ -173,8 +177,12 @@ public function testGetThumbnailInvalidSize(): void {
173177
}
174178

175179
public function testGetThumbnailInvalidImage(): void {
180+
$storage = $this->createMock(IStorage::class);
181+
$storage->method('instanceOfStorage')->with(ISharedStorage::class)->willReturn(false);
182+
176183
$file = $this->createMock(File::class);
177184
$file->method('getId')->willReturn(123);
185+
$file->method('getStorage')->willReturn($storage);
178186
$this->userFolder->method('get')
179187
->with($this->equalTo('unknown.jpg'))
180188
->willReturn($file);
@@ -196,9 +204,86 @@ public function testGetThumbnailInvalidPartFile(): void {
196204
$this->assertEquals($expected, $this->apiController->getThumbnail(10, 10, 'unknown.jpg'));
197205
}
198206

207+
public function testGetThumbnailSharedNoDownload(): void {
208+
$attributes = $this->createMock(IAttributes::class);
209+
$attributes->expects(self::once())
210+
->method('getAttribute')
211+
->with('permissions', 'download')
212+
->willReturn(false);
213+
214+
$share = $this->createMock(IShare::class);
215+
$share->expects(self::once())
216+
->method('getAttributes')
217+
->willReturn($attributes);
218+
219+
$storage = $this->createMock(ISharedStorage::class);
220+
$storage->expects(self::once())
221+
->method('instanceOfStorage')
222+
->with(ISharedStorage::class)
223+
->willReturn(true);
224+
$storage->expects(self::once())
225+
->method('getShare')
226+
->willReturn($share);
227+
228+
$file = $this->createMock(File::class);
229+
$file->method('getId')->willReturn(123);
230+
$file->method('getStorage')->willReturn($storage);
231+
232+
$this->userFolder->method('get')
233+
->with('unknown.jpg')
234+
->willReturn($file);
235+
236+
$this->preview->expects($this->never())
237+
->method('getPreview');
238+
239+
$expected = new DataResponse(['message' => 'File not found.'], Http::STATUS_NOT_FOUND);
240+
$this->assertEquals($expected, $this->apiController->getThumbnail(10, 10, 'unknown.jpg'));
241+
}
242+
243+
public function testGetThumbnailShared(): void {
244+
$share = $this->createMock(IShare::class);
245+
$share->expects(self::once())
246+
->method('getAttributes')
247+
->willReturn(null);
248+
249+
$storage = $this->createMock(ISharedStorage::class);
250+
$storage->expects(self::once())
251+
->method('instanceOfStorage')
252+
->with(ISharedStorage::class)
253+
->willReturn(true);
254+
$storage->expects(self::once())
255+
->method('getShare')
256+
->willReturn($share);
257+
258+
$file = $this->createMock(File::class);
259+
$file->method('getId')->willReturn(123);
260+
$file->method('getStorage')->willReturn($storage);
261+
262+
$this->userFolder->method('get')
263+
->with($this->equalTo('known.jpg'))
264+
->willReturn($file);
265+
$preview = $this->createMock(ISimpleFile::class);
266+
$preview->method('getName')->willReturn('my name');
267+
$preview->method('getMTime')->willReturn(42);
268+
$this->preview->expects($this->once())
269+
->method('getPreview')
270+
->with($this->equalTo($file), 10, 10, true)
271+
->willReturn($preview);
272+
273+
$ret = $this->apiController->getThumbnail(10, 10, 'known.jpg');
274+
275+
$this->assertEquals(Http::STATUS_OK, $ret->getStatus());
276+
$this->assertInstanceOf(FileDisplayResponse::class, $ret);
277+
}
278+
199279
public function testGetThumbnail(): void {
280+
$storage = $this->createMock(IStorage::class);
281+
$storage->method('instanceOfStorage')->with(ISharedStorage::class)->willReturn(false);
282+
200283
$file = $this->createMock(File::class);
201284
$file->method('getId')->willReturn(123);
285+
$file->method('getStorage')->willReturn($storage);
286+
202287
$this->userFolder->method('get')
203288
->with($this->equalTo('known.jpg'))
204289
->willReturn($file);

0 commit comments

Comments
 (0)