1

DISCLAIMER: Before creating this question, I've checked here, here and here, and also checked Laravel docs.

Context

  • Laravel 9 full-stack
  • No JS framework on front-end, which means I'm using vanilla JS
  • The folders on Storage are setted like this:
    • storage
      • app
        • public
          • folder1
            • folder1A
            • folder1B
            • folder1C
            • etc
  • The files stored in each folder1X are .pdf format and I don't know its names.
  • No folders are empty, nor with invalid/corrupted files.

The problem

I have a FileController.php to download files that are inside a folder1X/ directory. The method to download it is as follows:

public function downloadFileFromStorage(Request $request): mixed
{
    $dirpath = $request->dirpath; // dirpath = public/folder1/folder1X. 

    $files = Storage::allFiles($dirpath);

    return response()->download(storage_path('app\\' . $files[0]));
}

(Note: dirpath is sent in a axios request by client and is also fetched from database on a previous request)

My Javascript CLI needs to enable the download of this file. The download is enabled by clicking on a button. The button calls downloadPDF(dirpath) which works as follows:

function downloadPDF(dirpath) {
    axios.post('/download-pdf-file', { dirpath })
        .then(
            success => {
                const url = success.data
                const a = document.createElement('a')
                a.download = 'file.pdf'
                a.href = url
                a.click()
            },
            error => {
                console.log(error.response)
            }
        )
}

But, when I run this function, I get a about:blank#blocked error.

Attempts

  • Changed the a HTML DOM approach to a window.open(url) on client;
  • Changed response() to Storage::download($files[0], 'file-name.pdf'), and with this I also tried using Blob on client as follows:
success => {
    const blob = new Blob([success.data], { type: 'application/pdf' })
    const fileURL = URL.createObjectURL(blob)
    window.openURL(fileURL)
},
  • Also mixed Blob with the a HTML DOM approach;
  • Changed storage_path argument to /app/public/ before concatenating to $files[0].

UPDATE

Following tips from @BenGooding and @cengsemihsahin, I changed files to the following:

JS


// FileDownload is imported on a require() at the code beginning

function downloadPDF(dirpath) {
    axios({
        url: '/download-pdf-file',
        method: 'GET',
        responseType: 'blob',
        options: {
            body: { dirpath }
        }
    }).then(
        success => {
            FileDownload(success.data, 'nota-fiscal.pdf')
        }
    )
}

PHP:


public function downloadFileFromStorage(Request $request): mixed
{
    $dirpath = $request->dirpath; // dirpath = public/folder1/folder1X. 

    $files = Storage::allFiles($dirpath);
    return Storage::download($files[0], 'filename.pdf');
}

and now it downloads a corrupted PDF that can't be opened.

Fornazari
  • 51
  • 1
  • 7
  • 1
    Have you tried making your request with a **GET** request and not a **POST** request? – cengsemihsahin Dec 29 '22 at 20:45
  • https://stackoverflow.com/questions/41938718/how-to-download-files-using-axios – Ben Gooding Dec 29 '22 at 21:14
  • Gonna try your suggestions, guys. – Fornazari Dec 29 '22 at 21:27
  • @BenGooding, I did it on this fashion but it saves a corrupted file. The PDF on the folder `folder1X` is not corrupted. – Fornazari Dec 29 '22 at 21:51
  • @cengsemihsahin GET request worked to stop about:blank#blocked. Thanks – Fornazari Dec 29 '22 at 21:52
  • You should really add validation for `$request->dirpath`. If you don't restrict this to specific set of folders then users could pass any folder and extract files from your server. Secondly, if these files are in `public/` folder, then you should be able to open them with direct link to those files. Instead of returning file content trough download you can return file url which is dirpath + name. Frontend can then use that url to open the new tab `window.open(success.data.url, '_blank');` – ljubadr Dec 30 '22 at 04:56
  • Tried this as well. It gives 404. – Fornazari Dec 30 '22 at 11:36
  • @ljubadr (Note: dirpath is sent in a axios request by client and is also fetched from database on a previous request) I tried to say here that `$request->dirpath` is not obtained from a user input. User asks for a thing, and this thing fetches the file path from database 'on the background'. User doesn't know that the file path is there in the client. – Fornazari Dec 30 '22 at 12:11
  • 1
    @Fornazari in this case it doesn't matter where the input is coming from. If you endpoint accepts path as a string then malicious user can edit the request and send different path to abuse your endpoint. I would strongly recommend to use different approach. You can try different things 1. validate the path and only allow specific values (for example `public/*`). 2. accept an `id` value that was used in previous request and get the value from database. 3. generate the download url on the backend and return only that. User needs to request file download directly. I would recommend this approach – ljubadr Dec 31 '22 at 01:58

1 Answers1

0

Finally found the issue, and it was here:

axios({
    url: '/download-pdf-file',
    method: 'GET',
    responseType: 'blob',
    options: {            // here
        body: { dirpath } // here
    }
})

Laravel's Request arrow operator -> can't fetch a GET body sent through options (At least, not on $request->key fashion; see more about it here) thus making me download a corrupted file - it wasn't fetching any file on Laravel as it didn't get any path at all.

Here is the solution I came with:

As I want to get a file in a route that doesn't change except for the 1X at folder1X, I'm processing the path obtained and sending the 1X as a GET query param:

let folderNumber = dirpath.split('/')
folderNumber = folderNumber[folderNumber.length].replaceAll('/', '')

axios({
    url: '/download-pdf-file?folder=',
    method: 'GET',
    responseType: 'blob'
})

This way I don't pass the whole path to back-end and it's possible to get folderNumber by using $request->query():

public function downloadFileFromStorage(Request $request): mixed
{
    $folderNumber = $request->query('folderNumber');
    $folderPath = '/public/folder1/folder' . $folderNumber . '/';
    $files = Storage::allFiles($folderPath);
        
    return Storage::download($files[0], 'file-name.pdf');
}

In a nutshell:

  • To download files, use GET requests;
  • To send arguments within GET requests, use query parameters and fetch them with $request->query('keyname') (or find out another way. Good luck!);
Fornazari
  • 51
  • 1
  • 7