Merge pull request #14233 from uberbrady/prevent_svg_injection_with_fake_extensions_rebased

Fixes file upload XSS vulnerability [sc-24156]
This commit is contained in:
snipe 2024-02-08 14:56:59 +00:00 committed by GitHub
commit a3b96aff1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 136 additions and 261 deletions

View file

@ -4,28 +4,27 @@ namespace App\Http\Controllers\Accessories;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Accessory;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Accessory\HttpFoundation\JsonResponse;
use enshrined\svgSanitize\Sanitizer;
class AccessoriesFilesController extends Controller
{
/**
* Validates and stores files associated with a accessory.
*
* @todo Switch to using the AssetFileRequest form request validator.
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param AssetFileRequest $request
* @param UploadFileRequest $request
* @param int $accessoryId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(AssetFileRequest $request, $accessoryId = null)
public function store(UploadFileRequest $request, $accessoryId = null)
{
if (config('app.lock_passwords')) {
@ -45,30 +44,7 @@ class AccessoriesFilesController extends Controller
foreach ($request->file('file') as $file) {
$extension = $file->getClientOriginalExtension();
$file_name = 'accessory-'.$accessory->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension == 'svg') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/accessories/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/accessories/'.$file_name, file_get_contents($file));
}
$file_name = $request->handleFile('private_uploads/accessories/', 'accessory-'.$accessory->id, $file);
//Log the upload to the log
$accessory->logUpload($file_name, e($request->input('notes')));
}

View file

@ -3,26 +3,25 @@
namespace App\Http\Controllers;
use App\Helpers\StorageHelper;
use App\Http\Requests\AssetFileRequest;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\AssetModel;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use enshrined\svgSanitize\Sanitizer;
class AssetModelsFilesController extends Controller
{
/**
* Upload a file to the server.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param AssetFileRequest $request
* @param UploadFileRequest $request
* @param int $modelId
* @return Redirect
* @since [v1.0]
* @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function store(AssetFileRequest $request, $modelId = null)
public function store(UploadFileRequest $request, $modelId = null)
{
if (! $model = AssetModel::find($modelId)) {
return redirect()->route('models.index')->with('error', trans('admin/hardware/message.does_not_exist'));
@ -37,27 +36,7 @@ class AssetModelsFilesController extends Controller
foreach ($request->file('file') as $file) {
$extension = $file->getClientOriginalExtension();
$file_name = 'model-'.$model->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension=='svg') {
\Log::debug('This is an SVG');
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/assetmodels/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/assetmodels/'.$file_name, file_get_contents($file));
}
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$model->id,$file);
$model->logUpload($file_name, e($request->get('notes')));
}

View file

@ -4,26 +4,25 @@ namespace App\Http\Controllers\Assets;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use enshrined\svgSanitize\Sanitizer;
class AssetFilesController extends Controller
{
/**
* Upload a file to the server.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param AssetFileRequest $request
* @param UploadFileRequest $request
* @param int $assetId
* @return Redirect
* @since [v1.0]
* @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function store(AssetFileRequest $request, $assetId = null)
public function store(UploadFileRequest $request, $assetId = null)
{
if (! $asset = Asset::find($assetId)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
@ -37,28 +36,7 @@ class AssetFilesController extends Controller
}
foreach ($request->file('file') as $file) {
$extension = $file->getClientOriginalExtension();
$file_name = 'hardware-'.$asset->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension=='svg') {
\Log::debug('This is an SVG');
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/assets/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/assets/'.$file_name, file_get_contents($file));
}
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
$asset->logUpload($file_name, e($request->get('notes')));
}

View file

@ -4,28 +4,27 @@ namespace App\Http\Controllers\Components;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Component;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\JsonResponse;
use enshrined\svgSanitize\Sanitizer;
class ComponentsFilesController extends Controller
{
/**
* Validates and stores files associated with a component.
*
* @todo Switch to using the AssetFileRequest form request validator.
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param AssetFileRequest $request
* @param UploadFileRequest $request
* @param int $componentId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(AssetFileRequest $request, $componentId = null)
public function store(UploadFileRequest $request, $componentId = null)
{
if (config('app.lock_passwords')) {
@ -43,30 +42,7 @@ class ComponentsFilesController extends Controller
}
foreach ($request->file('file') as $file) {
$extension = $file->getClientOriginalExtension();
$file_name = 'component-'.$component->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension == 'svg') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/components/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/components/'.$file_name, file_get_contents($file));
}
$file_name = $request->handleFile('private_uploads/components/','component-'.$component->id, $file);
//Log the upload to the log
$component->logUpload($file_name, e($request->input('notes')));

View file

@ -4,28 +4,27 @@ namespace App\Http\Controllers\Consumables;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Consumable;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Consumable\HttpFoundation\JsonResponse;
use enshrined\svgSanitize\Sanitizer;
class ConsumablesFilesController extends Controller
{
/**
* Validates and stores files associated with a consumable.
*
* @todo Switch to using the AssetFileRequest form request validator.
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param AssetFileRequest $request
* @param UploadFileRequest $request
* @param int $consumableId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(AssetFileRequest $request, $consumableId = null)
public function store(UploadFileRequest $request, $consumableId = null)
{
if (config('app.lock_passwords')) {
return redirect()->route('consumables.show', ['consumable'=>$consumableId])->with('error', trans('general.feature_disabled'));
@ -42,30 +41,7 @@ class ConsumablesFilesController extends Controller
}
foreach ($request->file('file') as $file) {
$extension = $file->getClientOriginalExtension();
$file_name = 'consumable-'.$consumable->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension == 'svg') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/consumables/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/consumables/'.$file_name, file_get_contents($file));
}
$file_name = $request->handleFile('private_uploads/consumables/','consumable-'.$consumable->id, $file);
//Log the upload to the log
$consumable->logUpload($file_name, e($request->input('notes')));

View file

@ -4,28 +4,27 @@ namespace App\Http\Controllers\Licenses;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\License;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\JsonResponse;
use enshrined\svgSanitize\Sanitizer;
class LicenseFilesController extends Controller
{
/**
* Validates and stores files associated with a license.
*
* @todo Switch to using the AssetFileRequest form request validator.
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param AssetFileRequest $request
* @param UploadFileRequest $request
* @param int $licenseId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(AssetFileRequest $request, $licenseId = null)
public function store(UploadFileRequest $request, $licenseId = null)
{
$license = License::find($licenseId);
@ -38,30 +37,7 @@ class LicenseFilesController extends Controller
}
foreach ($request->file('file') as $file) {
$extension = $file->getClientOriginalExtension();
$file_name = 'license-'.$license->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension == 'svg') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/licenses/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/licenses/'.$file_name, file_get_contents($file));
}
$file_name = $request->handleFile('private_uploads/licenses/','license-'.$license->id, $file);
//Log the upload to the log
$license->logUpload($file_name, e($request->input('notes')));

View file

@ -4,14 +4,13 @@ namespace App\Http\Controllers\Users;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetFileRequest;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Support\Facades\Storage;
class UserFilesController extends Controller
@ -19,14 +18,14 @@ class UserFilesController extends Controller
/**
* Return JSON response with a list of user details for the getIndex() view.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.6]
* @param AssetFileRequest $request
* @param UploadFileRequest $request
* @param int $userId
* @return string JSON
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.6]
*/
public function store(AssetFileRequest $request, $userId = null)
public function store(UploadFileRequest $request, $userId = null)
{
$user = User::find($userId);
$destinationPath = config('app.private_uploads').'/users';
@ -41,31 +40,7 @@ class UserFilesController extends Controller
return redirect()->back()->with('error', trans('admin/users/message.upload.nofiles'));
}
foreach ($files as $file) {
$extension = $file->getClientOriginalExtension();
$file_name = 'user-'.$user->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
// Check for SVG and sanitize it
if ($extension == 'svg') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put('private_uploads/users/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
Storage::put('private_uploads/users/'.$file_name, file_get_contents($file));
}
$file_name = $request->handleFile('private_uploads/users/', 'user-'.$user->id, $file);
//Log the uploaded file to the log
$logAction = new Actionlog();

View file

@ -1,30 +0,0 @@
<?php
namespace App\Http\Requests;
class AssetFileRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$max_file_size = \App\Helpers\Helper::file_upload_max_size();
return [
'file.*' => 'required|mimes:png,gif,jpg,svg,jpeg,doc,docx,pdf,txt,zip,rar,xls,xlsx,lic,xml,rtf,json,webp|max:'.$max_file_size,
];
}
}

View file

@ -103,7 +103,25 @@ class ImageUploadRequest extends Request
\Log::info('File name will be: '.$file_name);
\Log::debug('File extension is: '.$ext);
if (($image->getClientOriginalExtension() !== 'webp') && ($image->getClientOriginalExtension() !== 'svg')) {
if ($image->getMimeType() == 'image/webp') {
// If the file is a webp, we need to just move it since webp support
// needs to be compiled into gd for resizing to be available
\Log::debug('This is a webp, just move it');
Storage::disk('public')->put($path.'/'.$file_name, file_get_contents($image));
} elseif($image->getMimeType() == 'image/svg+xml') {
// If the file is an SVG, we need to clean it and NOT encode it
\Log::debug('This is an SVG');
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($image->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::disk('public')->put($path . '/' . $file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug($e);
}
} else {
\Log::debug('Not an SVG or webp - resize');
\Log::debug('Trying to upload to: '.$path.'/'.$file_name);
@ -125,25 +143,6 @@ class ImageUploadRequest extends Request
// This requires a string instead of an object, so we use ($string)
Storage::disk('public')->put($path.'/'.$file_name, (string) $upload->encode());
} else {
// If the file is a webp, we need to just move it since webp support
// needs to be compiled into gd for resizing to be available
if ($image->getClientOriginalExtension() == 'webp') {
\Log::debug('This is a webp, just move it');
Storage::disk('public')->put($path.'/'.$file_name, file_get_contents($image));
// If the file is an SVG, we need to clean it and NOT encode it
} else {
\Log::debug('This is an SVG');
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($image->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::disk('public')->put($path.'/'.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug($e);
}
}
}
// Remove Current image if exists

View file

@ -0,0 +1,70 @@
<?php
namespace App\Http\Requests;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Support\Facades\Storage;
class UploadFileRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$max_file_size = \App\Helpers\Helper::file_upload_max_size();
return [
'file.*' => 'required|mimes:png,gif,jpg,svg,jpeg,doc,docx,pdf,txt,zip,rar,xls,xlsx,lic,xml,rtf,json,webp|max:'.$max_file_size,
];
}
/**
* Sanitizes (if needed) and Saves a file to the appropriate location
* Returns the 'short' (storage-relative) filename
*
* TODO - this has a lot of similarities to UploadImageRequest's handleImage; is there
* a way to merge them or extend one into the other?
*/
public function handleFile(string $dirname, string $name_prefix, $file): string
{
$extension = $file->getClientOriginalExtension();
$file_name = $name_prefix.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$file->guessExtension();
\Log::debug("Your filetype IS: ".$file->getMimeType());
// Check for SVG and sanitize it
if ($file->getMimeType() === 'image/svg+xml') {
\Log::debug('This is an SVG');
\Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put($dirname.$file_name, $cleanSVG);
} catch (\Exception $e) {
\Log::debug('Upload no workie :( ');
\Log::debug($e);
}
} else {
$put_results = Storage::put($dirname.$file_name, file_get_contents($file));
\Log::debug("Here are the '$put_results' (should be 0 or 1 or true or false or something?)");
}
return $file_name;
}
}

View file

@ -36,7 +36,7 @@
{!! $errors->first('image', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<div class="col-md-4 col-md-offset-3" aria-hidden="true">
<img id="uploadFile-imagePreview" style="max-width: 300px; display: none;" alt="{{ trans('partials/forms/general.alt_uploaded_image_thumbnail') }}">
<img id="uploadFile-imagePreview" style="max-width: 300px; display: none;" alt="{{ trans('general.alt_uploaded_image_thumbnail') }}">
</div>
</div>