Are laravel's routes safeguarding enough against file traversal attacks? - laravel

Route::get('/transaction/{name}', 'TransactionController#download');
public function download($name){
$path = storage_path('app/something/') . $name . '.xml';
return response()->download($path);
}
The user shall using this action only be able to download .xml files in app/something.
Is it possible to to download data outside of the specified app/something folder.

Laravel doesn't protect against traversal attacks - the router will return any value with your code example, meaning that someone could get access to your filesystem!
You an use PHP's basename() to sanitise $name by removing any path references from the string:
Route::get('/transaction/{name}', 'TransactionController#download');
public function download($name){
$path = storage_path('app/something/') . basename($name, '.xml') . '.xml';
return response()->download($path);
}

As far as i know Laravel will compile your path to:
#^/transaction/(?P<name>[^/]++)$#s
So simple / will not not work..
You could use more sophisticated backslash - but it depends on server..
At the end - Remember not to trust all user input.. No matter does it goes through routing or received directly..

Updated answer
As you can see below, it's definitely possible to do malicious stuff within Laravel routes. Given your function setup, the chance of someone doing something you don't want is small, because he/she can only alter the $name variable.
You can still write some extra code like this (found on viblo.asia):
$basepath = '/foo/bar/baz/'; // Path to xml file
$realBase = realpath($basepath);
$userpath = $basepath . $_GET['path'];
$realUserPath = realpath($userpath);
if ($realUserPath === false || strpos($realUserPath, $realBase) !== 0) {
//Directory Traversal!
} else {
//Good path!
}
To prevent users from accessing files they aren't allowed to.
Old, but relevant answer
Just tried this in Homestead:
Route::get(
'/',
function () {
dump(exec('ls ' . storage_path() . '/../../../'));
}
);
And that prints the corresponding folder just fine:
So I'd say that it's definitely possible to do stuff outside of the specified folder. Try this for yourself for example:
Route::get(
'/',
function () {
for ($i = 0; $i < 10; $i++) {
$path = str_repeat('/..', $i);
dump(exec('ls ' . storage_path() . $path));
}
}
);
And see your folders appear on screen when you hit the / route.

Related

Laravel 5.4 - File Storage returns false when checking for file existence

I am trying to see if a file exist in the storage folder. It returns false in the server even if the file exist. In my local environment, it is working perfect.
I am trying to store avatar for users. When I check if avatar exists, it returns false.
Store avatar:
$avatar = $request->file('avatar');
$user = Auth::user();
if($avatar && $file = $avatar->isValid()) {
$file = $request->file('avatar');
$path = $user->avatar_path;
$image = Image::make($file)->fit(100,100)->encode('jpg');
Storage::put($path, $image);
$url = Storage::url($path);
}
Checking for avatar existence
$user = Auth::user();
if ($user->has_avatar) {
Storage::delete($user->avatar_path);
}
has avatar method in user.php model
public function getHasAvatarAttribute()
{
return Storage::disk('public')->has('avatars/'.md5($this->id . $this->email).'.jpg');
}
Avatar path function
public function getAvatarPathAttribute()
{
return 'public/avatars/'. md5($this->id . $this->email) . '.jpg';
}
When I test on my machine, all is good. On server, it returns false even if avatar exists.
I can not comment Yet so I will leave this answer here. In production it is not working? As in a live server? Maybe linux server? And you are running the local development on Windows? See where I am going with this? Your path maybe wrong. I mean the naming. Even a to A or vice versa may effect the path. The same goes for your file naming. And path naming. avatar/something and /avatar/something are completely different.
Please let me know if that was the case.

Custom storage path per model

I'm implementing an email handling system where I want to save the original email and all its attachments to a path. For example - mail-data/123456/5
Where 123456 is the parent ID and 5 is the child ID.
In filesystems.php I've created a custom disk called mail-data
'mail-data' => [
'driver' => 'local',
'root' => storage_path('app/public/mail-data'),
'visibility' => 'private',
],
This works great as far as setting a prefix for the storage path, visibility etc. However, what I want to be able to do is on a per model basis, call a storage property and it return the mail-data driver set to the exact path. This way, all of my logic can simply be:
$model->storage->put($file->getFilename(), $file->stream());
rather than:
$path = Storage::disk('mail-data')->put($model->parent_id . '/' . $model->id . '/' . $file->getFilename(), $file->getStream())
I think the best way to do this by creating an accessor on the model, and I've been able to update the adapter, I just don't know how to update that on the Filesystem instance and return it?
public function getStorageAttribute()
{
$storage = Storage::disk('mail-data');
$adapter = $storage->getAdapter();
$adapter->setPathPrefix($adapter->getPathPrefix() . $this->parent_id . '/' . $this->id);
// what to do here to return our modified storage instance?
}
Right, I was a bit stupid here... it turns out that when you run setPathPrefix on the adapter, it's all by reference so the code above actually had the desired effect. For anyone Googling in the future, here is the final code -
On the model -
/**
* Get our storage disk for this model
*
* #return \Illuminate\Contracts\Filesystem\Filesystem
*/
public function getStorageAttribute()
{
$storage = Storage::disk('mail-data');
$adapter = $storage->getAdapter();
$adapter->setPathPrefix($adapter->getPathPrefix() . $this->ticket_id . '/' . $this->id);
return $storage;
}
I can then access my models storage at the absolute storage path simply using $model->storage. So my now much cleaner code for saving my mail data looks like this (no more calculating paths and having to worry about calculating paths anywhere else in my logic) -
$storage = $model->storage;
$storage->put('email.eml', $mail->message()->getStream());
/** #var MimePart $attachment */
foreach ($mail->attachments() as $attachment) {
$storage->put($attachment->getFilename(), $attachment->getStream());
}
Very happy with that solution and I hope it comes in handy for someone else in the future :)

Can't write image data to path in laravel

I am having the same error as this guy is :
Another thread
basically the error i have is uploading the image to the specific path, my code is as follows :
public function postCreate() {
$validator = Validator::make(Input::all() , Product::$rules);
if ($validator->passes()) {
$product = new Product;
$product->category_id = Input::get('category_id');
$product->title = Input::get('title');
$product->description = Input::get('description');
$product->price = Input::get('price');
$image = Input::file('image');
$filename = date('Y-m-d-H:i:s')."-".$image->getClientOriginalName();
Image::make($image->getRealPath())->resize(468, 249)->save('public/img/products'.$filename);
$product->image = 'public/img/products'.$filename;
$product->save();
return Redirect::to('admin/products/index')
->with('message' , 'Product created');
}
return Redirect::to('admin/products/index')->with('message' , 'something went wrong')
->withErrors($validator)->withInput();
}
I was just trying to follow a tutorial on laravel e-commerce web application.
I guess the problem is that i don't have write permisson in my directory , how do i add write permission in my directory. I.E. the public folder, I googled a few places , but i don't understand what is it that i have to edit ?
I.E the htaccesss file or can i make write changes on the cmd ? also how do i check what weather a directory is write protected .
PS. i am using windows . i am attaching a screenshot of the error .
Thank you.
You might want to change the dateformat since windows doesn't allow colons in filenames:
$filename = date('Y-m-d-H:i:s')."-".$image->getClientOriginalName();
And you also might want to add a trailing slash to your path so it doesn't concatenate the filename to the folder path:
Image::make($image->getRealPath())->resize(468, 249)->save('public/img/products'.$filename);
Generally this error occurs when you do not yet have the directory that will store the image inside the public directory. Sometimes it can be a permission issue.
Does you img directory exists in your public directory?
To fix this, follow the steps:
Use this snippet:
$relPath = 'img/'; //your path inside public directory
if (!file_exists(public_path($relPath))) { //Verify if the directory exists
mkdir(public_path($relPath), 666, true); //create it if do not exists
}
Or manually create the img directory in public
2.Then you can save your image:
Image::make($image)->resize(468, 249)->save(public_path('img/products'.$filename)); //save you image
$product->image = 'img/products'.$filename; //note
$product->save();
**NOTE: We do not need to specify the public directory in the path because we are using a relative path. The img directory will be created inside public directory.
Along with this, you need to make sure the folder path exists and which has right permissions set.
$relPath = 'img/product/';
if (!file_exists(public_path($relPath))) {
mkdir(public_path($relPath), 777, true);
}
Where $relPath is the path relative to public directory.
This requirement is however windows specific. In linux, folder directory will be created if it does not exist.
I also recommend all of you to check if $path exists. Like Jose Seie use native PHP check, I recommend you to thought about build-in helpers.
This can be achieved with File Facade helper:
File::exists($imagePath) or File::makeDirectory($imagePath, 777, true);
Advice you to use Laravel built-in functions, classes & helpers to improve the performance of your application!
well , i made the correction that john suggested and then made the following corrections :
I replaced the below code :
Image::make($image->getRealPath())->resize(468, 249)->save('public/img/products'.$filename);
with :
$path = public_path('img/products/'.$filename);
Image::make($image->getRealPath())->resize(468, 249)->save($path);
problem solved , i don't know why public_path works , but never mind .

response::download is doing nothing

I am having problems getting files to download outside of the public folder. Here is the Scenario:
These files cannot be accessed from anywhere but through this application and these downloads must go through access control. So a user can't download the file if they are not logged in and have permission to.
I have a route defined with a get variable. This ID goes into the controller and the controller calls the method below:
public static function downloadFile($id){
$file = FileManager::find($id);
//PDF file is stored under app/storage/files/
$download= app_path().substr($file->location,6);///home/coursesupport/public_html/app/storage/files/fom01/7-2/activities-and-demos.pdf
$fileName = substr($download,strrpos($download,'/')+1);//activities-and-demos.pdf
$mime = mime_content_type($download);//application/pdf
$headers = array(
'Content-Type: '.$mime,
"Content-Description" => "File Transfer",
"Content-Disposition" => "attachment; filename=" . $fileName
);
return Response::download($download, $fileName, $headers);
}
the problem is it does nothing, just opens a blank page. Am I missing something? oh yeah, and the link to the route mentioned above opens a blank tab.
I appreciate any help. Thanks!
It should work by doing only this:
public static function downloadFile($id)
{
$file = FileManager::find($id);
$download= app_path().substr($file->location,6);
$fileName = substr($download,strrpos($download,'/')+1);
return Response::download($download, $fileName);
}
Make sure your route is actually returning the returned response. Eg:
Route::get('download', function()
{
return DownloadHandler::downloadFile(1);
});
Without the return in the route the IlluminateResponse will do nothing.

Codeigniter Page cache with GET parameter

I am newbie to CI cache. I am facing some weird problem with codeigniter page caching. $this->output->cache(300);
I was expecting that cached version would not load if arguments in GET[] would change. But it is loading cache without considering any GET[] parameters.
I have one page where it says whether comment has been saved or not [via get parameter],
/product/product-name/?saved=true redirecting to same page where comment form is located. But it is not working. How can i invalidate old cache and create new one depending upon the get parameter? or i need to change the behavior of my comment system?
Thanks.
EDIT
Should i simply use database cache instead of Web page cache in this case?
You just have to enable the cache_query_string option in the config/config.php file.
$config['cache_query_string'] = TRUE;
Create a cache_override hook to check if there are any GET[] variables set and then skip the cache_override.
[EDIT #1]
Here is an example:
Create this file in your hooks directory:
<?php
class GetChecker {
public function checkForGet()
{
global $OUT, $CFG, $URI;
if (isset($_GET) AND ! empty($_GET))
{
return;
}
if ($OUT->_display_cache($CFG, $URI) == TRUE)
{
exit;
}
}
}
Then add this to the config/hooks.php:
$hook['cache_override'][] = array(
'class' => 'GetChecker',
'function' => 'checkForGet',
'filename' => 'GetChecker.php',
'filepath' => 'hooks'
);
I haven't tested it, it might need a little tweaking to work...
I test on CI 3+ , file system/core/Output.php 559 line, change this
if ($CI->config->item('cache_query_string') && !empty($_SERVER['QUERY_STRING']))
{
$uri .= '?'.$_SERVER['QUERY_STRING'];
}
on this
if ($CI->config->item('cache_query_string') /* && ! empty($_SERVER['QUERY_STRING']) */ && !empty($_REQUEST))
{
// $uri .= '?'.$_SERVER['QUERY_STRING'];
$uri .= '?'.http_build_query($_REQUEST);
}
And add string to your application/config/config.php
$config['cache_query_string'] = true;
it will be work with GET, POST, COOKIE ....
If need only GET, just $config['cache_query_string'] = true; - enough
I found no easier way using Hooks to prevent writing cache, as its calling _write_cache() inside the _display() method itself of CI_Output class.
For quick and easiest solution I added two conditions to display cache and write cache, if Query String parameter has variable defined( offset in my case, as I wanted for pagination)
Edit: system/core/Output.php
Add Condition to prevent writing cache, if specific GET parameter found:
function _write_cache($output)
{
if (isset($_GET['offset']) AND ! empty($_GET['offset']))
{
log_message('debug', " Don't write cache please. As as its matching condition");
return;
}
...
...
}
Add Condition to prevent displaying cache, if specific GET parameter found:
function _display_cache(&$CFG, &$URI)
{
if (isset($_GET['offset']) AND ! empty($_GET['offset']))
{
log_message('debug', " Don't display cache please. As as its matching condition");
return FALSE;
}
...
...
}

Resources