Secure your Gravity Forms uploads

I love WordPress. I like Gravity Forms. The only trouble with Gravity forms is if you add a file upload field to your form it is stored within your /wp-content/uploads directory and as such is publicly accessible.

Now the link that Gravity Forms generates is fairly long (mywebsite.com/wp-content/uploads/gravity_forms/reallylongstringofnumbers10239812039802193/year/month/filename) however that just isn’t secure enough for me. The worst part os they don’t seem to care.

After a week or two of searching I have found a way of securing it! The following method secures the folder in question so that only Logged in users can access them. It is also probably possible to modify this method to account for different user roles etc, but for now a little security is better than none!

Ok so we need to do 2 things.

  1. Tell our website that all files in a certain directory need to be passed to a special file for processing before downloading.
  2. Use our special file to check if the user is logged in!
First things first. Create a new file in the ROOT of your WordPress install called dl-file.php and paste in the following:
<?php
require_once('wp-load.php');
is_user_logged_in() || auth_redirect();
$upload_dir = wp_upload_dir();
//Set your path below I am using /gravity_forms/
$basedir = $upload_dir[ 'basedir' ] . '/gravity_forms/';

$file = rtrim( $basedir, '/' ) . '/' . str_replace( '..', '', isset( $_GET[ 'file' ] ) ? $_GET[ 'file' ] : '' );
if ( ! $basedir || ! is_file( $file ) ) {
 status_header( 404 );
 die( '404 &#8212; File not found.' );
}

$mime = wp_check_filetype( $file );
if( false === $mime[ 'type' ] && function_exists( 'mime_content_type' ) )
 $mime[ 'type' ] = mime_content_type( $file );

if( $mime[ 'type' ] )
 $mimetype = $mime[ 'type' ];
else
 $mimetype = 'image/' . substr( $file, strrpos( $file, '.' ) + 1 );

header( 'Content-Type: ' . $mimetype ); // always send this
if ( false === strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS' ) )
 header( 'Content-Length: ' . filesize( $file ) );

$last_modified = gmdate( 'D, d M Y H:i:s', filemtime( $file ) );
$etag = '"' . md5( $last_modified ) . '"';
header( "Last-Modified: $last_modified GMT" );
header( 'ETag: ' . $etag );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + 100000000 ) . ' GMT' );

// Support for Conditional GET
$client_etag = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? stripslashes( $_SERVER['HTTP_IF_NONE_MATCH'] ) : false;

if( ! isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) )
 $_SERVER['HTTP_IF_MODIFIED_SINCE'] = false;

$client_last_modified = trim( $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
// If string is empty, return 0. If not, attempt to parse into a timestamp
$client_modified_timestamp = $client_last_modified ? strtotime( $client_last_modified ) : 0;

// Make a timestamp for our most recent modification...
$modified_timestamp = strtotime($last_modified);

if ( ( $client_last_modified && $client_etag )
 ? ( ( $client_modified_timestamp >= $modified_timestamp) && ( $client_etag == $etag ) )
 : ( ( $client_modified_timestamp >= $modified_timestamp) || ( $client_etag == $etag ) )
 ) {
 status_header( 304 );
 exit;
}

// If we made it this far, just serve the file
readfile( $file );

Looks kinda scary doesn’t it! Don’t worry about it! The only thing you need to pay attention to is line 8. This is where you set which folder you are protecting, for this I am using /gravity_forms/ the path is relative to your uploads directory.

Second in your .htaccess file you need to add the following:


#Gravity Forms Upload Protection
RewriteCond %{REQUEST_FILENAME} -s
RewriteRule ^wp-content/uploads/gravity_forms/(.*)$ dl-file.php?file=$1 [QSA,L]

The RewriteRule path needs to match the path you set in your dl-file.php so here you can see gravity_forms is mentioned again.

So what happens.

When a file is requested from within the Gravity Forms folder the request is passed via the .htaccess file to our dl-file.php. This file then checks to see if the user is logged in. If not logged in they are forwarded to the login page, after logging in the file is downloaded. If they don’t login. No file. Simple!

There are probably loads of improvements that can be made to this but it is a great start.

The credit goes entirely to http://wordpress.stackexchange.com/a/37743/12438 I simply took the code and made it work for me! Let me know what improvements you make to it!

44 comments

  1. 2021 and this is still a viable solution! It works in Chrome and Firefox, however Safari (on Mac and iPads) reports ‘cannot parse response”…

  2. Will your solution work with gf-download?

    eg. index.php?gf-download=2017%2F11%2Ftest-upload.pdf&form-id=47&field-id=2&hash=b33b1995b8d3a6fbc23eec70401c9362c6990ffa0b50dd9c951821d23a537a6c

  3. this is brilliant. It is prompting me to login when I click on the upload icon or view .pdf. if i go to the name of the individual then view, it passes through the attachment with out the prompt.
    It will not accept my password

    1. disregard.
      Trevor Gehman 2 years ago
      Great solution to a terrible oversight by Gravity Forms. I changed the authentication portion to this, so that it just throws a 404 error instead of redirecting to the login page:

      if(!is_user_logged_in()) {
      status_header( 404 );
      nocache_headers();
      include( get_query_template( ‘404’ ) );
      die();
      }
      fixed it. works great!

  4. Hi, thanks a lot for this solution !
    I just had to activate the rewrite_mod in my httpd.conf and everything worked great 🙂

  5. I think I am the only one with this problem but this is not working for me. I have placed .dl-file.php into the root of the WP install, modified the root’s .htaccess as directed, but am still able to see the files just fine. I also tried modifying the path on line 6 to /gravity_forms/ (to include /wp-content/uploads) but that also did not help.

    Does this secure all subdirectories as well? Because as you know Gravity Forms places form submissions into its own sub-directory by date, like 3-227c1900b53af23b0bf2e3f9df5a0b40/2015/12

    Suggestions?

    1. Update: Here is what DOES work.. Replacing the lines in .htaccess to this worked:

      #disallow access to file uploads unless user is logged in
      RewriteCond %{HTTP_COOKIE} !^.*wordpress_logged_in_.*$
      RewriteRule ^wp-content/uploads/gravity_forms/(.*)$ http://path-to-your-error-page.com/ [NC,R,L]

  6. Hi, great solution, thanks. I am just not clear on which .htaccess file needs to be modified with the 3 lines of code. The .htaccess file in the root folder of the WordPress installation or the .htaccess file that is located in the upload/gravity-forms folder?

    Thanks.

  7. Hi, thanks for that nice script. But its not working on multisite installation right? Do you have a solution how to protect the folder on multisites?

  8. It would be a minor improvement to pass a message to the login page that reads somethng like “You must be logged in to view this file”. Otherwise – great solution, thanks a lot.

    1. Thanks I’m glad this is still useful to people! I’m surprised that after all this time it still hasn’t made it’s way into the Gravity Forms core!

  9. Great solution to a terrible oversight by Gravity Forms. I changed the authentication portion to this, so that it just throws a 404 error instead of redirecting to the login page:

    if(!is_user_logged_in()) {
    status_header( 404 );
    nocache_headers();
    include( get_query_template( '404' ) );
    die();
    }

  10. This is great. I would rather it redirected to the login page (with wp_login_url() set to redirect to the file). I’m still trying to get into the code, let me know if you have a fix for that handy.

  11. Hi – just wanted to say that I got this working now and it’s wonderful! I even used a modified version of your PHP file to password protect some additional directories unrelated to gravity forms. This was hugely useful and I am very grateful.

  12. Thanks! Working as expected. If you haven’t already sent this on to the Gravity Forms developers, please do. There’s no reason this shouldn’t already be included in the setup options or among their documentation.

  13. I just tried this and it did not work. I was still able to access uploaded documents even when not logged in. No idea why. Is there any specific place within .htaccess where the 3 lines of text should go?

      1. Not sure I understand… the path in line 6 of dl-file.php? You have it as ‘/gravity_forms/’
        Do I need the full path, ralative to the wordpress root?

          1. The path is relative to your WordPress uploads folder so in my example wp-content/uploads/gravity_forms.

            You only need to change it if you are using a directory named something different to gravity_forms.

          2. I appreciate the help, thanks. It still did not work for me, but I have a feeling I have some deeper issues that need to be fixed. If your method works for 90% of users, you have a great idea here!

  14. This is a great post and has been very useful to me. Thanks. One thing worth pointing out though is that if you use the Really Simple Captcha integration with Gravity Forms then it stores the generated png in the gravity_forms directory, as this script stands it will not be displayed on the front end.

    I modified it slightly so there is a check up front for the filetype to see if it’s a .png and then just serving the file if it is. ie;

    require_once('wp-load.php');
    
    $upload_dir = wp_upload_dir();
    //Set your path below I am using /gravity_forms/
    $basedir = $upload_dir[ 'basedir' ] . '/gravity_forms/';
    
    $file = rtrim( $basedir, '/' ) . '/' . str_replace( '..', '', isset( $_GET[ 'file' ] ) ? $_GET[ 'file' ] : '' );
    $mime = wp_check_filetype( $file );
    
    if ($mime[ 'ext' ] == 'png') {
    
    	// Its the captcha , we just serve the file with a basic header
    	header('Content-type: image/jpeg');
    	readfile($file);
    	die();
    }
    
    is_user_logged_in() || auth_redirect();

    Hope this helps, cheers.

  15. Thanks for putting this together! Saved me lots of time searching the web! Noticed that the copy on github is giving an error because it is missing:

    require_once('wp-load.php');

    I also modified it to only allow access to the gravity_forms folder if the user is an admin by replacing:

    is_user_logged_in() || auth_redirect();

    to include the user role:

    ( (is_user_logged_in() ) && (current_user_can('administrator')) ) || auth_redirect();

  16. I have uploaded the dl-file.php to the root of the wp installation, and made the following alterations (see below) to my .htaccess file – but when I navigate to the uploaded file in the gravity forms directory I don’t get redirected. Instead, my browsers just show the text from the the dl-file.php in a paragraph. I have messed around with this for hours, please tell me I’m overlooking something really simple here? Thanks, Owain

    #Gravity Forms Upload Protection
    RewriteCond %{REQUEST_FILENAME} -s
    RewriteRule ^wp-content/uploads/gravity_forms/(.*)$ dl-file.php?file=$1 [QSA,L]
    
    # Use PHP5 Single php.ini as default
    AddHandler application/x-httpd-php5s .php
    
    # BEGIN WordPress
    
    RewriteEngine On
    RewriteBase /
    RewriteRule ^index.php$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.php [L]
    
    
    # END WordPress
    AuthName "topsete1"
    AuthUserFile "/home2/topsete1/.htpasswds/public_html/passwd"
    1. Nothing I can see there although I did notice that in my code example there was a closing peer tag at the start for some reason (probably from when I changed syntax highlighters). Do you have that in your code if so remove it. And make sure you have a opening php tag at the start of the file as well!

        1. Sorry for the confusion I made my reply from my iPad which auto-corrected everything I said.

          Ok there were 2 issues with my code example.

          1) On line 1 there was a

          </pre>

          tag which shouldn’t have been there.
          2) I didn’t including the opening

          <?php

          tags as I hoped everyone would insert that themselves.

          I’ve fixed the code example above and have also put the code on GitHub so you can simply get the whole thing. Let me know if you get this working!

          GitHub Gist

      1. Hey man,

        I’ve used Super Cache in the past and it def. does write to the .htaccess file. I’m way more of a theme author than plugin developer, so I have no idea how it is actually accomplished. But I have seen it happen.

        Also, the Retina @2x plugin writes to the .htaccess file as well.

        And of course, thanks for the code!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.