skip to Main Content

My PHP script has difficulty downloading PNG and JPG images from a folder (outside of the root), resulting in corrupted files. PDF files, on the other hand, can be downloaded without issues. I have confirmed that the files exist, have the correct MIME types (image/png and image/jpeg), and are not empty (file size not empty).

The files also exist in the uploads folder, and I can open them with preview tools, etc., so the file in the server works as intended, and I suspect I set my headers wrong.

The file download gets triggered when the file is clicked that starts a page load with GET parameters (I’m using a front controller)

container.addEventListener('click', () => {

    // ... Get the user id and file name ...

    // Construct the URL to the PHP script that gets the correct file
    const open = `/view/?user=${user}&file=${file}&open=true`;

    // Open in a new page (used for inline, not attachments)
    window.open(open, '_blank');
});

My view file:

// Make sure the user is authenticated...

// If download, user, and file parameters are set, download the file:
if(isset($_GET['open']) && isset($_GET['user']) && isset($_GET['file'])){
    if(!Files::open($_GET['file'], $_GET['user'])){
        Report::error("Error");
    }

    Report::success("Success");
    exit();
}

My Files::open() function:

/**
 * Open a requested file.
 * 
 * @param string $file
 * The file to open.
 * 
 * @param string $folder
 * The folder where the file is located.
 * 
 * @return bool
 * True if the file was opened successfully, false otherwise.
 */
public static function open(string $file, string $folder = ''): bool {

    // Update: 16.05.2023 - Quickfix for vulnerability:
    $file = str_replace(['../', '..\'], '', $file);

    // Path to the file, this currently only picks
    // files from the uploads folder:
    $path = dirname(__DIR__, 2) . "/uploads/$folder/$file";
    Report::notice("Opening the file '$file' from the '$folder' folder.");

    // If the file does not exist, return false:
    if (!file_exists($path)) {
        Report::warning("The file '$file' does not exist in the '$folder' folder.");
        return false;
    }

    // Create a new Fileinfo object:
    $finfo = new finfo(FILEINFO_MIME_TYPE);

    // Get the mime type:
    $mime = $finfo->file($path);

    // Get the mime type:
    Report::notice("The mime type of the file '$file' is '$mime'.");

    // Set the headers (usually inline, attachment for testing):
    header("Content-Type: $mime");
    header("Content-Disposition: attachment; filename=$file");
    header("Content-Length: " . filesize($path));

    // Read the file
    readfile($path);

    // Return true:
    return true;
}

UPDATE 16.05.2023

Thanks to the assistance provided in the comment section, we have made progress in identifying the issue. It has been determined that when the file is downloaded, additional information is added to the file. Specifically, a leading whitespace is consistently included in all downloaded files. Interestingly, the PDF format appears to be able to disregard this extra information, as mentioned in the comments.

2

Answers


  1. Chosen as BEST ANSWER

    As SalmanA noted, the issue was in each file's added newline character, that in fact, came from a newline in my view.php script, which had an accidental newline before opening tags for php <php.

    I also managed to fix the issue by cleaning the output buffer.

    // ...
    
    // Prevent any output buffering
    while (ob_get_level()) {
        ob_end_clean();
    }
    
    // Read the file
    readfile($path);
    

  2. Seems like your PHP script is adding a LF character (line feed, Unix line ending character, 0x0A). This could be caused by having a blank line somewhere in your script for example:

    
    <?php
    echo "ABCD";
    # the output of this program will be 0D 0A 41 42 43 44
    # or 0A 41 42 43 44 depending on line ending used
    

    Locate the blank line and remove it.

    To provide context, the original file’s first byte was FFD8FFE0,
    whereas the downloaded file’s first was 0AFFD8FF

    FFD8FFE0 are the first four bytes of a JPEG image (ref); you seem to have 0AFFD8FFE0 in the beginning where the 0A is the LF character.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search