Skip to content

Commit 256bc63

Browse files
committed
Fixes RCE in uploads controllers
1 parent eceb0a3 commit 256bc63

3 files changed

Lines changed: 127 additions & 8 deletions

File tree

app/Http/Controllers/EmailTemplatesController.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Http\Request;
66
use App\Models\Setting;
77
use Illuminate\Support\Facades\Validator;
8+
use App\Utils\FileHelper;
89

910
class EmailTemplatesController extends Controller
1011
{
@@ -94,12 +95,22 @@ public function update(Request $request)
9495
{
9596
$templates = $request->all();
9697
foreach ($templates as $template) {
98+
// Validate template ID to prevent path traversal attacks
99+
$templateId = $template['id'] ?? '';
100+
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $templateId)) {
101+
return response()->json([
102+
'status' => 'error',
103+
'message' => 'Invalid template ID',
104+
'data' => []
105+
], 400);
106+
}
107+
97108
// Build dynamic validation rules based on original template
98-
$originalPath = resource_path('views/emails/' . $template['id'] . '.twig');
109+
$originalPath = resource_path('views/emails/' . $templateId . '.twig');
99110
if (!file_exists($originalPath)) {
100111
return response()->json([
101112
'status' => 'error',
102-
'message' => 'Template not found: ' . $template['id'],
113+
'message' => 'Template not found: ' . $templateId,
103114
'data' => []
104115
], 404);
105116
}
@@ -144,9 +155,13 @@ public function update(Request $request)
144155

145156
private function updateTemplate($template)
146157
{
147-
$template_file_path = storage_path('templates/emails/' . $template['id'] . '.twig');
158+
// Template ID should already be validated by update() method
159+
// but add basename() as defense-in-depth
160+
$safeId = basename($template['id']);
161+
162+
$template_file_path = storage_path('templates/emails/' . $safeId . '.twig');
148163
Setting::updateOrCreate(
149-
['key' => 'email_subject_' . $template['id'] . '.twig'],
164+
['key' => 'email_subject_' . $safeId . '.twig'],
150165
['value' => $template['subject'], 'group' => 'system.emails.subjects']
151166
);
152167
if (!file_exists(dirname($template_file_path))) {

app/Http/Controllers/TusdHooksController.php

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -422,12 +422,38 @@ protected function handleBundleUpload(string $uploadId, UploadSession $session,
422422
$fileIds = [];
423423
foreach ($manifest['files'] as $fileInfo) {
424424
$filePath = $fileInfo['path'];
425-
$extractedFilePath = $extractDir . '/' . $filePath;
425+
426+
// Sanitize the file path to prevent path traversal attacks
427+
// Remove ../ sequences and normalize path
428+
$safePath = $this->sanitizeBundlePath($filePath);
429+
if ($safePath === null) {
430+
Log::warning('tusd post-finish: Skipping file with dangerous path', [
431+
'upload_id' => $uploadId,
432+
'file_path' => $filePath
433+
]);
434+
continue;
435+
}
436+
437+
$extractedFilePath = $extractDir . '/' . $safePath;
438+
439+
// Verify the resolved path is within the extraction directory
440+
$resolvedPath = realpath($extractedFilePath);
441+
$resolvedExtractDir = realpath($extractDir);
442+
443+
if ($resolvedPath === false || $resolvedExtractDir === false ||
444+
strpos($resolvedPath, $resolvedExtractDir) !== 0) {
445+
Log::warning('tusd post-finish: Path traversal attempt in bundle', [
446+
'upload_id' => $uploadId,
447+
'file_path' => $filePath,
448+
'resolved_path' => $resolvedPath
449+
]);
450+
continue;
451+
}
426452

427453
if (!file_exists($extractedFilePath)) {
428454
Log::warning('tusd post-finish: Bundle file not found after extraction', [
429455
'upload_id' => $uploadId,
430-
'file_path' => $filePath
456+
'file_path' => $safePath
431457
]);
432458
continue;
433459
}
@@ -440,7 +466,7 @@ protected function handleBundleUpload(string $uploadId, UploadSession $session,
440466
'original_name' => $fileInfo['originalName'],
441467
'type' => $fileInfo['type'] ?? 'application/octet-stream',
442468
'size' => $fileInfo['size'],
443-
'temp_path' => 'uploads/' . $uploadId . '_extracted/' . $filePath
469+
'temp_path' => 'uploads/' . $uploadId . '_extracted/' . $safePath
444470
]);
445471

446472
$fileIds[] = $file->id;
@@ -531,6 +557,37 @@ protected function postTerminate(Request $request, array $payload)
531557
}
532558
}
533559

560+
/**
561+
* Sanitize a bundle file path to prevent path traversal attacks
562+
* Returns null if the path is dangerous and should be skipped
563+
*/
564+
private function sanitizeBundlePath(string $path): ?string
565+
{
566+
// Normalize directory separators
567+
$path = str_replace('\\', '/', $path);
568+
569+
// Check for dangerous patterns before sanitization
570+
if (strpos($path, '..') !== false) {
571+
return null;
572+
}
573+
574+
// Remove leading slashes
575+
$path = ltrim($path, '/');
576+
577+
// Remove null bytes
578+
$path = str_replace("\0", '', $path);
579+
580+
// Clean up double slashes
581+
$path = preg_replace('/\/+/', '/', $path);
582+
583+
// If empty after sanitization, reject
584+
if (empty($path)) {
585+
return null;
586+
}
587+
588+
return $path;
589+
}
590+
534591
/**
535592
* Format bytes into human readable format
536593
*/

app/Http/Controllers/UploadsController.php

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,13 +279,33 @@ public function createShareFromUploads(Request $request)
279279
$originalPath = $request->filePaths[$uploadId] ?? '';
280280
$originalPath = explode('/', $originalPath);
281281
$originalPath = implode('/', array_slice($originalPath, 0, -1));
282+
283+
// Sanitize path to prevent directory traversal attacks
284+
$originalPath = $this->sanitizePath($originalPath);
282285
}
283286

284287
$destPath = $completePath . '/' . $originalPath;
285-
288+
289+
// Verify the resolved path is within the share directory
290+
// Create parent directories first so realpath can resolve
286291
if (!file_exists($destPath)) {
287292
mkdir($destPath, 0777, true);
288293
}
294+
$resolvedPath = realpath($destPath);
295+
$resolvedSharePath = realpath($completePath);
296+
297+
if ($resolvedPath === false || $resolvedSharePath === false ||
298+
strpos($resolvedPath, $resolvedSharePath) !== 0) {
299+
Log::warning('Path traversal attempt detected', [
300+
'user_id' => $user->id,
301+
'original_path' => $request->filePaths[$uploadId] ?? '',
302+
'resolved_path' => $resolvedPath,
303+
'share_path' => $resolvedSharePath
304+
]);
305+
306+
// Clean up and skip this file
307+
continue;
308+
}
289309

290310
// Use sanitized filename from database for file operations
291311
$sanitizedFilename = $file->name;
@@ -429,6 +449,33 @@ private function sendShareCreatedEmail(Share $share, $recipient)
429449
}
430450
}
431451

452+
/**
453+
* Sanitize a file path to prevent directory traversal attacks
454+
* Removes ../, ..\, and leading slashes
455+
*/
456+
private function sanitizePath(string $path): string
457+
{
458+
// Normalize directory separators
459+
$path = str_replace('\\', '/', $path);
460+
461+
// Remove any ../ or ..\ sequences (handles encoded variants too)
462+
$path = preg_replace('/\.\.[\\/\\\\]/', '', $path);
463+
464+
// Also remove standalone ..
465+
$path = preg_replace('/\.\./', '', $path);
466+
467+
// Remove leading slashes to prevent absolute paths
468+
$path = ltrim($path, '/');
469+
470+
// Remove any null bytes
471+
$path = str_replace("\0", '', $path);
472+
473+
// Clean up any double slashes that may have resulted
474+
$path = preg_replace('/\/+/', '/', $path);
475+
476+
return $path;
477+
}
478+
432479
/**
433480
* Recursively delete a directory and its contents
434481
*/

0 commit comments

Comments
 (0)