Skip to content

Commit 74de2ee

Browse files
committed
fixup
1 parent f5c1801 commit 74de2ee

13 files changed

Lines changed: 343 additions & 209 deletions

File tree

config/config.sample.php

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1976,7 +1976,7 @@
19761976
'updatedirectory' => '',
19771977

19781978
/**
1979-
* Deny a specific file or files and disallow the upload of files
1979+
* Block a specific file or files and disallow the upload of files
19801980
* with this name. ``.htaccess`` is blocked by default.
19811981
*
19821982
* WARNING: USE THIS ONLY IF YOU KNOW WHAT YOU ARE DOING.
@@ -1985,19 +1985,10 @@
19851985
*
19861986
* Defaults to ``array('.htaccess')``
19871987
*/
1988-
'blacklisted_files' => ['.htaccess'],
1988+
'forbidden_filenames' => ['.htaccess'],
19891989

19901990
/**
1991-
* Deny extensions from being used for filenames.
1992-
*
1993-
* The '.part' extension is always forbidden, as this is used internally by Nextcloud.
1994-
*
1995-
* Defaults to ``array('.filepart', '.part')``
1996-
*/
1997-
'forbidden_filename_extensions' => ['.part', '.filepart'],
1998-
1999-
/**
2000-
* Deny characters from being used in filenames. This is useful if you
1991+
* Block characters from being used in filenames. This is useful if you
20011992
* have a filesystem or OS which does not support certain characters like windows.
20021993
*
20031994
* The '/' and '\' characters are always forbidden, as well as all characters in the ASCII range [0-31].
@@ -2007,7 +1998,16 @@
20071998
*
20081999
* Defaults to ``array()``
20092000
*/
2010-
'forbidden_chars' => [],
2001+
'forbidden_filename_characters' => [],
2002+
2003+
/**
2004+
* Deny extensions from being used for filenames.
2005+
*
2006+
* The '.part' extension is always forbidden, as this is used internally by Nextcloud.
2007+
*
2008+
* Defaults to ``array('.filepart', '.part')``
2009+
*/
2010+
'forbidden_filename_extensions' => ['.part', '.filepart'],
20112011

20122012
/**
20132013
* If you are applying a theme to Nextcloud, enter the name of the theme here.

lib/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@
378378
'OCP\\Files\\ForbiddenException' => $baseDir . '/lib/public/Files/ForbiddenException.php',
379379
'OCP\\Files\\GenericFileException' => $baseDir . '/lib/public/Files/GenericFileException.php',
380380
'OCP\\Files\\IAppData' => $baseDir . '/lib/public/Files/IAppData.php',
381+
'OCP\\Files\\IFilenameValidator' => $baseDir . '/lib/public/Files/IFilenameValidator.php',
381382
'OCP\\Files\\IHomeStorage' => $baseDir . '/lib/public/Files/IHomeStorage.php',
382383
'OCP\\Files\\IMimeTypeDetector' => $baseDir . '/lib/public/Files/IMimeTypeDetector.php',
383384
'OCP\\Files\\IMimeTypeLoader' => $baseDir . '/lib/public/Files/IMimeTypeLoader.php',
@@ -1438,6 +1439,7 @@
14381439
'OC\\Files\\Config\\UserMountCache' => $baseDir . '/lib/private/Files/Config/UserMountCache.php',
14391440
'OC\\Files\\Config\\UserMountCacheListener' => $baseDir . '/lib/private/Files/Config/UserMountCacheListener.php',
14401441
'OC\\Files\\FileInfo' => $baseDir . '/lib/private/Files/FileInfo.php',
1442+
'OC\\Files\\FilenameValidator' => $baseDir . '/lib/private/Files/FilenameValidator.php',
14411443
'OC\\Files\\Filesystem' => $baseDir . '/lib/private/Files/Filesystem.php',
14421444
'OC\\Files\\Lock\\LockManager' => $baseDir . '/lib/private/Files/Lock/LockManager.php',
14431445
'OC\\Files\\Mount\\CacheMountProvider' => $baseDir . '/lib/private/Files/Mount/CacheMountProvider.php',

lib/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
411411
'OCP\\Files\\ForbiddenException' => __DIR__ . '/../../..' . '/lib/public/Files/ForbiddenException.php',
412412
'OCP\\Files\\GenericFileException' => __DIR__ . '/../../..' . '/lib/public/Files/GenericFileException.php',
413413
'OCP\\Files\\IAppData' => __DIR__ . '/../../..' . '/lib/public/Files/IAppData.php',
414+
'OCP\\Files\\IFilenameValidator' => __DIR__ . '/../../..' . '/lib/public/Files/IFilenameValidator.php',
414415
'OCP\\Files\\IHomeStorage' => __DIR__ . '/../../..' . '/lib/public/Files/IHomeStorage.php',
415416
'OCP\\Files\\IMimeTypeDetector' => __DIR__ . '/../../..' . '/lib/public/Files/IMimeTypeDetector.php',
416417
'OCP\\Files\\IMimeTypeLoader' => __DIR__ . '/../../..' . '/lib/public/Files/IMimeTypeLoader.php',
@@ -1471,6 +1472,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
14711472
'OC\\Files\\Config\\UserMountCache' => __DIR__ . '/../../..' . '/lib/private/Files/Config/UserMountCache.php',
14721473
'OC\\Files\\Config\\UserMountCacheListener' => __DIR__ . '/../../..' . '/lib/private/Files/Config/UserMountCacheListener.php',
14731474
'OC\\Files\\FileInfo' => __DIR__ . '/../../..' . '/lib/private/Files/FileInfo.php',
1475+
'OC\\Files\\FilenameValidator' => __DIR__ . '/../../..' . '/lib/private/Files/FilenameValidator.php',
14741476
'OC\\Files\\Filesystem' => __DIR__ . '/../../..' . '/lib/private/Files/Filesystem.php',
14751477
'OC\\Files\\Lock\\LockManager' => __DIR__ . '/../../..' . '/lib/private/Files/Lock/LockManager.php',
14761478
'OC\\Files\\Mount\\CacheMountProvider' => __DIR__ . '/../../..' . '/lib/private/Files/Mount/CacheMountProvider.php',
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
namespace OC\Files;
9+
10+
use OC\L10N\L10N;
11+
use OCP\Files\EmptyFileNameException;
12+
use OCP\Files\FileNameTooLongException;
13+
use OCP\Files\IFilenameValidator;
14+
use OCP\Files\InvalidCharacterInPathException;
15+
use OCP\Files\InvalidPathException;
16+
use OCP\Files\ReservedWordException;
17+
use OCP\IConfig;
18+
use OCP\L10N\IFactory;
19+
use Psr\Log\LoggerInterface;
20+
21+
/**
22+
* @since 30.0.0
23+
*/
24+
class FilenameValidator implements IFilenameValidator {
25+
26+
private L10N $l10n;
27+
28+
/**
29+
* @var list<string>
30+
*/
31+
private array $forbiddenNames = [];
32+
33+
/**
34+
* @var list<string>
35+
*/
36+
private array $forbiddenCharacters = [];
37+
38+
/**
39+
* @var list<string>
40+
*/
41+
private array $forbiddenExtensions = [];
42+
43+
public function __construct(
44+
IFactory $l10nFactory,
45+
private IConfig $config,
46+
private LoggerInterface $logger,
47+
) {
48+
$this->l10n = $l10nFactory->get('core');
49+
}
50+
51+
/**
52+
* Get a list of reserved filenames that must not be used
53+
* This list should be checked case-insensitive, all names are returned lowercase.
54+
* @return list<string>
55+
* @since 30.0.0
56+
*/
57+
public function getForbiddenExtensions(): array {
58+
if (empty($this->forbiddenExtensions)) {
59+
$forbiddenExtensions = $this->config->getSystemValue('forbidden_filename_extensions', ['.filepart']);
60+
if (!is_array($forbiddenExtensions)) {
61+
$this->logger->error('Invalid system config value for "forbidden_filename_extensions" is ignored.');
62+
$forbiddenExtensions = ['.filepart'];
63+
}
64+
65+
// Always forbid .part files as they are used internally
66+
$forbiddenExtensions = array_merge($forbiddenExtensions, ['.part']);
67+
68+
// The list is case insensitive so we provide it always lowercase
69+
$forbiddenExtensions = array_map('mb_strtolower', $forbiddenExtensions);
70+
$this->forbiddenExtensions = array_values($forbiddenExtensions);
71+
}
72+
return $this->forbiddenExtensions;
73+
}
74+
75+
/**
76+
* Get a list of forbidden filename extensions that must not be used
77+
* This list should be checked case-insensitive, all names are returned lowercase.
78+
* @return list<string>
79+
* @since 30.0.0
80+
*/
81+
public function getForbiddenFilenames(): array {
82+
if (empty($this->forbiddenNames)) {
83+
$forbiddenNames = $this->config->getSystemValue('forbidden_filenames', ['.htaccess']);
84+
if (!is_array($forbiddenNames)) {
85+
$this->logger->error('Invalid system config value for "forbidden_filenames" is ignored.');
86+
$forbiddenNames = ['.htaccess'];
87+
}
88+
89+
// Handle legacy config option
90+
// TODO: Drop with Nextcloud 34
91+
$legacyForbiddenNames = $this->config->getSystemValue('blacklisted_files', []);
92+
if (!is_array($legacyForbiddenNames)) {
93+
$this->logger->error('Invalid system config value for "blacklisted_files" is ignored.');
94+
$legacyForbiddenNames = [];
95+
}
96+
if (!empty($legacyForbiddenNames)) {
97+
$this->logger->warning('System config option "blacklisted_files" is deprecated and will be removed in Nextcloud 34, use "forbidden_filenames" instead.');
98+
}
99+
$forbiddenNames = array_merge($legacyForbiddenNames, $forbiddenNames);
100+
101+
// The list is case insensitive so we provide it always lowercase
102+
$forbiddenNames = array_map('mb_strtolower', $forbiddenNames);
103+
$this->forbiddenNames = array_values($forbiddenNames);
104+
}
105+
return $this->forbiddenNames;
106+
}
107+
108+
/**
109+
* Get a list of characters forbidden in filenames
110+
*
111+
* Note: Characters in the range [0-31] are always forbidden,
112+
* even if not inside this list (see OCP\Files\Storage\IStorage::verifyPath).
113+
*
114+
* @return list<string>
115+
* @since 30.0.0
116+
*/
117+
public function getForbiddenCharacters(): array {
118+
if (empty($this->forbiddenCharacters)) {
119+
// Get always forbidden characters
120+
$forbiddenCharacters = str_split(\OCP\Constants::FILENAME_INVALID_CHARS);
121+
if ($forbiddenCharacters === false) {
122+
$forbiddenCharacters = [];
123+
}
124+
125+
// Get admin defined invalid characters
126+
$additionalChars = $this->config->getSystemValue('forbidden_chars', []);
127+
if (!is_array($additionalChars)) {
128+
$this->logger->error('Invalid system config value for "forbidden_chars" is ignored.');
129+
$additionalChars = [];
130+
}
131+
$forbiddenCharacters = array_merge($forbiddenCharacters, $additionalChars);
132+
133+
// Handle legacy config option
134+
// TODO: Drop with Nextcloud 34
135+
$legacyForbiddenCharacters = $this->config->getSystemValue('forbidden_chars', []);
136+
if (!is_array($legacyForbiddenCharacters)) {
137+
$this->logger->error('Invalid system config value for "forbidden_chars" is ignored.');
138+
$legacyForbiddenCharacters = [];
139+
}
140+
if (!empty($legacyForbiddenCharacters)) {
141+
$this->logger->warning('System config option "forbidden_chars" is deprecated and will be removed in Nextcloud 34, use "forbidden_filename_characters" instead.');
142+
}
143+
$forbiddenCharacters = array_merge($legacyForbiddenCharacters, $forbiddenCharacters);
144+
145+
$this->forbiddenCharacters = array_values($forbiddenCharacters);
146+
}
147+
return $this->forbiddenCharacters;
148+
}
149+
150+
/**
151+
* @inheritdoc
152+
*/
153+
public function isFilenameValid(string $filename): bool {
154+
try {
155+
$this->validateFilename($filename);
156+
} catch (\OCP\Files\InvalidPathException) {
157+
return false;
158+
}
159+
return true;
160+
}
161+
162+
/**
163+
* @inheritdoc
164+
*/
165+
public function validateFilename(string $filename): void {
166+
// Ensure we are working on the filename
167+
$filename = \OC\Files\Filesystem::normalizePath($filename);
168+
$filename = basename($filename);
169+
170+
$trimmed = trim($filename);
171+
if ($trimmed === '') {
172+
throw new EmptyFileNameException();
173+
}
174+
175+
// the special directories . and .. would cause never ending recursion
176+
if ($trimmed === '.' || $trimmed === '..') {
177+
throw new ReservedWordException();
178+
}
179+
180+
// 255 characters is the limit on common file systems (ext/xfs)
181+
// oc_filecache has a 250 char length limit for the filename
182+
if (isset($fileName[250])) {
183+
throw new FileNameTooLongException();
184+
}
185+
186+
if ($this->isForbidden($filename)) {
187+
throw new ReservedWordException();
188+
}
189+
190+
$this->checkForbiddenExtension($filename);
191+
192+
$this->checkForbiddenCharacters($filename);
193+
}
194+
195+
/**
196+
* Check if the filename is forbidden
197+
* @param string $filename
198+
* @return bool True if invalid name, False otherwise
199+
*/
200+
public function isForbidden(string $filename): bool {
201+
$filename = mb_strtolower($filename);
202+
203+
if ($filename === '') {
204+
return false;
205+
}
206+
207+
// The name part without extension
208+
$basename = substr($filename, 0, strpos($filename, '.', 1) ?: null);
209+
// Check for forbidden filenames
210+
$forbiddenNames = $this->getForbiddenFilenames();
211+
if (in_array($basename, $forbiddenNames)) {
212+
return true;
213+
}
214+
215+
// Filename is not forbidden
216+
return false;
217+
}
218+
219+
/**
220+
* Check if a filename contains any of the forbidden characters
221+
* @param string $filename
222+
* @throws InvalidCharacterInPathException
223+
*/
224+
protected function checkForbiddenCharacters(string $filename): void {
225+
$sanitizedFileName = filter_var($filename, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW);
226+
if ($sanitizedFileName !== $filename) {
227+
throw new InvalidCharacterInPathException();
228+
}
229+
230+
foreach ($this->getForbiddenCharacters() as $char) {
231+
if (str_contains($filename, $char)) {
232+
throw new InvalidCharacterInPathException($char);
233+
}
234+
}
235+
}
236+
237+
/**
238+
* Check if a filename has a forbidden filename extension
239+
* @param string $filename The filename to validate
240+
* @throws InvalidPathException
241+
*/
242+
protected function checkForbiddenExtension(string $filename): void {
243+
$filename = mb_strtolower($filename);
244+
// Check for forbidden filename exten<sions
245+
$forbiddenExtensions = $this->getForbiddenExtensions();
246+
foreach ($forbiddenExtensions as $extension) {
247+
if (str_ends_with($filename, $extension)) {
248+
throw new InvalidPathException($this->l10n->t('Invalid filename extension "%1$s"', [$extension]));
249+
}
250+
}
251+
}
252+
};

lib/private/Files/Filesystem.php

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -425,62 +425,6 @@ public static function isValidPath($path) {
425425
return true;
426426
}
427427

428-
/**
429-
* @param string $filename
430-
* @return bool
431-
*/
432-
public static function hasFilenameInvalidCharacters(string $filename): bool {
433-
$invalidChars = \OCP\Util::getForbiddenFileNameChars();
434-
foreach ($invalidChars as $char) {
435-
if (str_contains($filename, $char)) {
436-
return true;
437-
}
438-
}
439-
440-
$sanitizedFileName = filter_var($filename, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW);
441-
if ($sanitizedFileName !== $filename) {
442-
return true;
443-
}
444-
return false;
445-
}
446-
447-
public static function hasFilenameInvalidExtension(string $filename): bool {
448-
$filename = mb_strtolower($filename);
449-
// Check for forbidden filename exten<sions
450-
$forbiddenExtensions = \OCP\Util::getForbiddenFilenameExtensions();
451-
foreach ($forbiddenExtensions as $extension) {
452-
if (str_ends_with($filename, $extension)) {
453-
return true;
454-
}
455-
}
456-
return false;
457-
}
458-
459-
/**
460-
* @param string $filename
461-
* @return bool True if invalid name, False otherwise
462-
*/
463-
public static function isFileBlacklisted(string $filename): bool {
464-
$filename = self::normalizePath($filename);
465-
$filename = basename($filename);
466-
$filename = mb_strtolower($filename);
467-
468-
if ($filename === '') {
469-
return false;
470-
}
471-
472-
// Check for forbidden filenames
473-
$forbiddenNames = \OCP\Util::getForbiddenFilenames();
474-
// The name part without extension
475-
$basename = substr($filename, 0, strpos($filename, '.', 1) ?: null);
476-
if (in_array($basename, $forbiddenNames)) {
477-
return true;
478-
}
479-
480-
// Filename is not forbidden
481-
return false;
482-
}
483-
484428
/**
485429
* check if the directory should be ignored when scanning
486430
* NOTE: the special directories . and .. would cause never ending recursion

0 commit comments

Comments
 (0)