Skip to content

Commit 9ed1a38

Browse files
authored
release(v3.18.0): file operation hardening
- security(upload): reject shared-upload filename collisions and require edit authority for replacements - security(metadata): use collision-resistant folder metadata filenames
1 parent 74ccb4a commit 9ed1a38

14 files changed

Lines changed: 594 additions & 107 deletions

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# Changelog
22

3+
## Changes 06/23/2026 (v3.18.0)
4+
5+
`release(v3.18.0): file operation hardening`
6+
7+
**Commit message**
8+
9+
```text
10+
release(v3.18.0): file operation hardening
11+
12+
- security(upload): reject shared-upload filename collisions and require edit authority for replacements
13+
- security(metadata): use collision-resistant folder metadata filenames
14+
```
15+
16+
**Fixed**
17+
18+
- **Upload overwrite hardening**
19+
- Public shared-folder and portal uploads are now create-only when the destination filename already exists.
20+
- Authenticated uploads now require edit authority plus ownership, folder ownership, admin, or bypass ownership before replacing an existing file.
21+
- Upload collision errors preserve the original file content and return an explicit conflict/permission response.
22+
23+
- **Folder metadata isolation hardening**
24+
- Folder metadata filenames now preserve distinct logical folder paths instead of flattening separators and spaces into the same name.
25+
- Ownership and `read_own` checks now resolve metadata from the same isolated path used by uploads, file operations, WebDAV, and background transfer handling.
26+
- Simple non-colliding folder metadata filenames remain compatible; existing metadata for collision-prone folders is copied into the isolated filename on first access.
27+
28+
---
29+
330
## Changes 06/20/2026 (v3.17.0)
431

532
`release(v3.17.0): phpseclib dependency security update`

src/FileRise/Domain/FileModel.php

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use FileRise\Support\ACL;
66
use FileRise\Support\CryptoAtRest;
77
use FileRise\Support\FS;
8+
use FileRise\Support\MetadataPath;
89
use FileRise\Support\UploadNamePolicy;
910
use FileRise\Support\WorkerLauncher;
1011
use FileRise\Storage\StorageAdapterInterface;
@@ -65,11 +66,7 @@ private static function metaRootForId(string $sourceId): string
6566

6667
private static function getMetadataFilePathForRoot(string $metaRoot, string $folder): string
6768
{
68-
$metaRoot = rtrim($metaRoot, "/\\") . DIRECTORY_SEPARATOR;
69-
if (strtolower($folder) === 'root' || trim($folder) === '') {
70-
return $metaRoot . "root_metadata.json";
71-
}
72-
return $metaRoot . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
69+
return MetadataPath::path($metaRoot, $folder);
7370
}
7471

7572
private static function trashRoot(): string
@@ -1188,8 +1185,7 @@ public static function renameFile($folder, $oldName, $newName)
11881185
// Perform the rename.
11891186
if ($storage->move($oldPath, $newPath)) {
11901187
// Update the metadata file.
1191-
$metadataKey = ($folder === 'root') ? "root" : $folder;
1192-
$metadataFile = self::metaRoot() . str_replace(['/', '\\', ' '], '-', trim($metadataKey)) . '_metadata.json';
1188+
$metadataFile = self::getMetadataFilePath($folder);
11931189

11941190
if (file_exists($metadataFile)) {
11951191
$metadata = json_decode(file_get_contents($metadataFile), true);
@@ -1300,9 +1296,7 @@ public static function saveFile(string $folder, string $fileName, $content, ?str
13001296
}
13011297

13021298
// Metadata
1303-
$metadataKey = strtolower($folder) === "root" ? "root" : $folder;
1304-
$metadataFileName = str_replace(['/', '\\', ' '], '-', trim($metadataKey)) . '_metadata.json';
1305-
$metadataFilePath = self::metaRoot() . $metadataFileName;
1299+
$metadataFilePath = self::getMetadataFilePath($folder);
13061300

13071301
$metadata = file_exists($metadataFilePath) ? (json_decode(file_get_contents($metadataFilePath), true) ?: []) : [];
13081302

@@ -3213,12 +3207,7 @@ public static function restoreFiles(array $trashFiles)
32133207
}
32143208

32153209
// Helper to get metadata file path for a folder.
3216-
$getMetadataFilePath = function ($folder) {
3217-
if (strtolower($folder) === 'root' || trim($folder) === '') {
3218-
return self::metaRoot() . "root_metadata.json";
3219-
}
3220-
return self::metaRoot() . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
3221-
};
3210+
$getMetadataFilePath = static fn($folder): string => self::getMetadataFilePath((string)$folder);
32223211

32233212
// Process each provided trash file name.
32243213
foreach ($trashFiles as $trashFileName) {
@@ -3524,9 +3513,7 @@ public static function saveFileTag(string $folder, string $file, array $tags, bo
35243513
$tags = self::sanitizeTags($tags);
35253514

35263515
// Determine the folder metadata file.
3527-
$metadataFile = (strtolower($folder) === "root")
3528-
? self::metaRoot() . "root_metadata.json"
3529-
: self::metaRoot() . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
3516+
$metadataFile = self::getMetadataFilePath($folder);
35303517

35313518
// Load existing metadata for this folder.
35323519
$metadata = [];
@@ -3650,15 +3637,7 @@ public static function getFileList(string $folder, array $options = []): array
36503637
return ["error" => "Invalid folder name."];
36513638
}
36523639

3653-
// Helper: Build the metadata file path.
3654-
$getMetadataFilePath = function (string $folder): string {
3655-
$metaRoot = self::metaRoot();
3656-
if (strtolower($folder) === 'root' || trim($folder) === '') {
3657-
return $metaRoot . "root_metadata.json";
3658-
}
3659-
return $metaRoot . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
3660-
};
3661-
$metadataFile = $getMetadataFilePath($folder);
3640+
$metadataFile = self::getMetadataFilePath($folder);
36623641
$metadata = file_exists($metadataFile) ? (json_decode(file_get_contents($metadataFile), true) ?: []) : [];
36633642

36643643
$dirStat = $storage->stat($directory);
@@ -4031,9 +4010,7 @@ public static function createFile(string $folder, string $filename, string $uplo
40314010
}
40324011

40334012
// 5) write metadata
4034-
$metaKey = (strtolower($folder) === 'root' || trim($folder) === '') ? 'root' : $folder;
4035-
$metaName = str_replace(['/', '\\', ' '], '-', $metaKey) . '_metadata.json';
4036-
$metaPath = self::metaRoot() . $metaName;
4013+
$metaPath = self::getMetadataFilePath($folder);
40374014

40384015
$collection = [];
40394016
if (file_exists($metaPath)) {

src/FileRise/Domain/FolderModel.php

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use FileRise\Support\ACL;
66
use FileRise\Support\CryptoAtRest;
77
use FileRise\Support\FS;
8+
use FileRise\Support\MetadataPath;
89
use FileRise\Support\UploadNamePolicy;
910
use FileRise\Storage\StorageAdapterInterface;
1011
use FileRise\Storage\SourceContext;
@@ -1492,10 +1493,7 @@ public static function moveFolderAcrossSources(
14921493
/** Build metadata file path for a given (relative) folder. */
14931494
private static function getMetadataFilePath(string $folder): string
14941495
{
1495-
if (strtolower($folder) === 'root' || trim($folder) === '') {
1496-
return self::metaRoot() . "root_metadata.json";
1497-
}
1498-
return self::metaRoot() . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json';
1496+
return MetadataPath::path(self::metaRoot(), $folder);
14991497
}
15001498

15011499
/**
@@ -1634,14 +1632,7 @@ public static function deleteFolderRecursiveAdmin(string $folder): array
16341632

16351633
// Remove metadata JSONs for this subtree
16361634
$relative = trim($relative, "/\\ ");
1637-
if ($relative !== '' && $relative !== 'root') {
1638-
$prefix = str_replace(['/', '\\', ' '], '-', $relative);
1639-
$globPat = self::metaRoot() . $prefix . '*_metadata.json';
1640-
$metaFiles = glob($globPat) ?: [];
1641-
foreach ($metaFiles as $mf) {
1642-
@unlink($mf);
1643-
}
1644-
}
1635+
MetadataPath::deleteSubtree(self::metaRoot(), $relative);
16451636

16461637
// Remove ownership mappings for the subtree.
16471638
self::removeOwnerForTree($relative);
@@ -1711,14 +1702,7 @@ private static function deleteFolderRecursiveAdminRemote(string $folder): array
17111702

17121703
// Remove metadata JSONs for this subtree
17131704
$relative = trim($relative, "/\\ ");
1714-
if ($relative !== '' && $relative !== 'root') {
1715-
$prefix = str_replace(['/', '\\', ' '], '-', $relative);
1716-
$globPat = self::metaRoot() . $prefix . '*_metadata.json';
1717-
$metaFiles = glob($globPat) ?: [];
1718-
foreach ($metaFiles as $mf) {
1719-
@unlink($mf);
1720-
}
1721-
}
1705+
MetadataPath::deleteSubtree(self::metaRoot(), $relative);
17221706

17231707
// Remove ownership mappings for the subtree.
17241708
self::removeOwnerForTree($relative);
@@ -1884,17 +1868,7 @@ public static function renameFolder(string $oldFolder, string $newFolder): array
18841868
}
18851869

18861870
// Update metadata filenames (prefix-rename)
1887-
$oldPrefix = str_replace(['/', '\\', ' '], '-', $oldRel);
1888-
$newPrefix = str_replace(['/', '\\', ' '], '-', $newRel);
1889-
$globPat = self::metaRoot() . $oldPrefix . '*_metadata.json';
1890-
$metadataFiles = glob($globPat) ?: [];
1891-
1892-
foreach ($metadataFiles as $oldMetaFile) {
1893-
$baseName = basename($oldMetaFile);
1894-
$newBase = preg_replace('/^' . preg_quote($oldPrefix, '/') . '/', $newPrefix, $baseName);
1895-
$newMeta = self::metaRoot() . $newBase;
1896-
@rename($oldMetaFile, $newMeta);
1897-
}
1871+
MetadataPath::renameSubtree(self::metaRoot(), $oldRel, $newRel);
18981872

18991873
// Update ownership mapping for the entire subtree.
19001874
self::renameOwnersForTree($oldRel, $newRel);

src/FileRise/Domain/McpCoreOpsService.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use FileRise\Storage\StorageAdapterInterface;
99
use FileRise\Storage\StorageRegistry;
1010
use FileRise\Support\ACL;
11+
use FileRise\Support\MetadataPath;
1112
use RuntimeException;
1213
use Throwable;
1314

@@ -2057,13 +2058,7 @@ private static function folderMetadataPath(string $folder): string
20572058
: rtrim((string)META_DIR, '/\\') . DIRECTORY_SEPARATOR;
20582059

20592060
$folder = ACL::normalizeFolder($folder);
2060-
if ($folder === 'root') {
2061-
return rtrim($metaRoot, '/\\') . DIRECTORY_SEPARATOR . 'root_metadata.json';
2062-
}
2063-
2064-
return rtrim($metaRoot, '/\\') . DIRECTORY_SEPARATOR
2065-
. str_replace(['/', '\\', ' '], '-', $folder)
2066-
. '_metadata.json';
2061+
return MetadataPath::path($metaRoot, $folder);
20672062
}
20682063

20692064
private static function normalizeFolder(string $folder): string

0 commit comments

Comments
 (0)