Prevent direct access to wordpress videos (Prevent direct access files)

Asked

Viewed 671 times

1

I own a wordpress-based Learning application, and I have several video lessons in the directory public_html/wp-content/uploads/...

I would like to prevent direct access to these videos, since it is necessary to purchase the course to be able to attend them.

In studying the question I came across the following propositions of solution to this problem:

Solution 1: Block requests coming from the WEB, using a file .htaccess, to my video files (.mp4) and serve them to the client with PHP, through the function readfile(). This way I could check if the user can access the file before serving it. This solution is functional, but I did not like it, because I believe that the server’s resource consumption will be very high, since PHP will be reading/processing the files.

In this case I would add a file .htaccess in wp-content/uploads/ with the following Directives:

     Rewriterule \.mp4$ dl-file.php [L]

And would use a script (dl-file.php) analogous to this to serve the video file :

 /*
 * dl-file.php
 *
 * Protect uploaded files with login.
 *
 * @link http://wordpress.stackexchange.com/questions/37144/protect-wordpress-uploads-if-user-is-not-logged-in
 *
 * @author hakre <http://hakre.wordpress.com/>
 * @license GPL-3.0+
 * @registry SPDX
 */

require_once('wp-load.php');

is_user_logged_in() ||  auth_redirect();

list($basedir) = array_values(array_intersect_key(wp_upload_dir(), array('basedir' => 1)))+array(NULL);

$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 );

In this case the video files would not be outside of public_html, since I would still need to assess the consequences of this, for my Wordpress application, and still the impacts under some utilities I use, based on the internal API of the WP-CLI.

Solution 2: Using an apache module called mod_xsendfile to serve the file, after checking, with PHP, whether or not the user can get access to that video. mod_xsendfile is an unofficial module, from a third party, and the latest update dates from 2012. Because of these factors, added to the fact that the project does not have an active community, I chose not to use this solution.

Solution 3: Prevent direct access through a file .htacess performing header checking %{HTTP_REFERER}. This is not a reliable way, since I must allow, for usability reasons, access when %{HTTP_REFERER} is also empty, that is, just a customer modify %{HTTP_REFERER} to have access to video lessons (.mp4) private. If the value of %{HTTP_REFERER} is reliable, 100% of the time, this would be a good solution, because the delivery of the video file would be conditioned to my application, which decides whether or not the user should access the class.

Solution 4: Using a plugin called Prevent Direct Access imagem do cabeçalho do plugin no repositório oficial do worpress To deliver the file to the user the plugin uses a script very similar to the one displayed on solution 1, which also made it unviable, plus the cost of the license is high. But in general it is a good solution when the files to be made available are not very large.

Solution 5: Finally I imagined a possible solution to my problem, but I still lack the necessary knowledge to verify its applicability, follows:

Whenever a user performs a direct request to a video file I perform a flag check through a file .htaccess. As for this flag I’m still not sure what it might be, a parameter get, or an apache environment variable, or a header.

If this flag has a given value I send the file, or if it has another value, other than necessary for the file to be sent, it is not sent to the user. Finally if this flag does not exist, at the time of checking, I rewrite the URL with a directive RewriteRule which will point to a PHP script.

This script checks whether or not the user can access the video file (checks if it is logged in and if it has purchased the course, basically) and then arrow the respective flag, and make a new request to the video file, which will cause the .htaccess previously run again, but now the flag exists as it was set in the script. Thus the file .htaccess will find the flag set, and depending on the value decides whether or not to deliver the requested file to the user. After sending the reply the flag is then destroyed.

As I mentioned, I cannot say if this is possible, and what structures to use. It is a supposed solution, which I need to check if it is possible. If not how I could do to prevent direct access to videos, remembering to consider the solutions I have already found and discarded previously.

  • put . htaccess com: Deny from all

  • But in this case how would I serve the user the videos? Would I have to read them with PHP?

  • 1

    About 2, although it is not something very up-to-date (after all, it’s quite simple, it doesn’t have anything to update), it has no problem using X-Sendfile in Apache currently. What was the difficulty found? Inclusive, has available ready for many distros. Ex: https://centos.pkgs.org/7/epel-x86_64/mod_xsendfile-0.12-10.el7.x86_64.rpm.html. - The only way I see for you is to actually allow access after login, either by X-Sendfile or even by a PHP serving file saved outside the site root (which is the normal and simple to do in such cases).

  • Thanks for the time @Bacco. I checked solution 1 in part, and it proved to be functional, even though, theoretically, it appears to be very costly to my eyes. Why do you consider it ineffective ? Regarding the use of X-SendFile, after researches, I came across some opinions contrary to its use ( this for example) and although, theoretically the ideal solution for me this made me back a little. Have you used this module? As for "a PHP serving file recorded outside the site root" in this case I would have to process the file, which I wish to avoid.

  • 1

    Use X-Sendfile constantly to relieve PHP’s work, but remember that if you don’t want to use it, you have the readfile(); PHP itself, which can get files out of the root without problem (which is partially its 1). I only prefer the module precisely to not have PHP processing the stream for no reason if I can leave it to Apache. About the link, it is an opinion, of a person, that may be valid, but has to relativize a bit.

  • 1

    About 1, I talked about htaccess, not readfile() - in the sense that it is better to put the files out of the root, instead of having to create locks. I don’t understand what you mean by "having to process the files" in that context.

  • @Bacco, I get it. I’ll turn to the mod_xsendfile study, based on your settings. I’ll run some tests to verify its applicability in my case. If you can formalize your statements as an answer to the main question I can classify it later and use it in the question’s Edit. Thanks for your help.

  • @Allandantas if you prefer to keep the readfile, as long as the server has resources slack, I see no problem, but the fundamental thing is with sendfile or readfile, is the file is outside the root instead of just blocked

  • @Bacco, Wordpress keeps your media inside a directory on public_html, would have to analyze the implications of this change. The solution I have in mind is to use FilesMatch to identify when a request is being made to a file .mp4 and then transfer the responsibility of responding to a PHP script, which would send the file with X-Sendfile. What you think about ?

  • I keep thinking the same, that it’s better to take it from the root

Show 6 more comments

1 answer

2

Place the files you want to protect in a subdirectory of the directory in which your code is running:

www.foo.com/player.html
www.foo.com/videos/video.mp4

Save a file in this subdirectory called ". htaccess" and add the lines below:

www.foo.com/videos/. htaccess

Contents of . htaccess:

RewriteEngine on
RewriteCond %{HTTP_REFERER} !^http://foo.com/.*$ [NC]
RewriteCond %{HTTP_REFERER} !^http://www.foo.com/.*$ [NC]
RewriteRule .(mp4|mp3|avi)$ - [F]

Now the source link will be fake, but we still need to ensure that any user who is trying to download the file cannot directly receive the file.

For a more complete solution, place your video with a fake screen (or html screen) and never link directly to the video. To disable right-click add to your HTML:

<body oncontextmenu="return false;">

The result:

www.foo.com/player.html will play the video correctly, but if you visit www.foo.com/videos/video.mp4 will receive an error message:

Error Code 403: FORBIDDEN

NOTE: This will work for direct download, Curl, hotlinking, etc.

To make it even harder, you can send a request to the server through a temporary md5 hash, and return your video through this temporary instance, so you won’t have a full video path inside your fake screen when this hash expires.

Example file (load_video.php):

//digamos que você tem isso no banco de dados
$videos = [
 ['directory' => 'videos','file' => 'video_nome_1', 'type' => 'mp4', 'id' => 1],
 ['directory' => 'videos','file' => 'video_nome_2', 'type' => 'mp4', 'id' => 2]
 ['directory' => 'videos','file' => 'video_nome_3', 'type' => 'mp4', 'id' => 3]
];

$data_list_videos = [];

foreach($videos as $k => $video) {
    $data_list_videos[md5($video['id']. range('a', 'd'))] = $video;
}

if($_POST) {
   $validate = (new DateTime())->getTimestamp();  
   $video = $data_list_videos[$_POST['video']];
   return json_encode([
     'status' => true,
     'video' => base64_encode($video),
     'validate' => $validate
   ]); 
}

In your view you could call something like this with ID 2 for example, and set the video path in your element #leitor, in case I’m using the jquery library to make a post:

<script>
     <?php $id = md5(2 . range('a', 'd')); ?>

            $.post('/load_video.php?video=<?php echo $id?>',function(rtn) {
                var data = JSON.parse(rtn);
                  if(data.status && (<?php echo (new DateTime())->getTimestamp();?> == data.validate)) {

                   var result = data.video;
                   var data_video = window.atob(result);
                  var srcVideo = [
                       '/',
                       data_video.directory,
                       '/',
                       data_video.file,
                       '.',
                       data_video.type,
                       '?',
                       validate=data.validate
                  ];
             var leitor = document.querySelector('#leitor');
             $.get('/leitor.php?video='+window.btoa(srcVideo.join(''))+'&validate='+data.validate, function(srcVideo) { 
                   leitor.src = JSON.parse(srcVideo).src;
                   leitor.setAttribute('type', 'video/'+data_video.type);
             });

       }
 });
</script>
    <video width="320" height="240" controls>
      <source id="leitor">
    </video>

In the video file, it can be a leitor.php you load a video player it returns the url:

/leitor.php?validate=1572363160&video=L3ZpZGVvL3ZpZGVvX25vbWVfMi5tcDQ=

   if ($_GET['validate'] == (new DateTime())->getTimestamp()) {
       echo json_decode(['src' => base64_decode($_GET['video'])]);
    }
  • 1

    Thanks for your time Ivan. A user could not change %{HTTP_REFERER} ? There are proxy applications that send %{HTTP_REFERER} empty, this way a user who bought the course but has one of these applications running on your machine would not have problems while accessing the video ?

  • As for the use of the hash, particularly, I can’t see any other way to send the file to the user, after solving it, if not through PHP. I am mistaken ?

  • 2

    @Allandantas any curious changes the REFERER without any problem. In general, REFERER does not protect anything important. The most that can be avoided is Hotlinks where the "smart guy" has no control over the third-party browser, but does not protect content. Example of valid use of REFERER: minimize large-scale bandwidth consumption (vc allows only blank or equal to the site, and discards third-party websites). Example of invalid use is your case, where the attacker has control of the download process.

  • @Allandantas, you can also instead of passing the video file, pass the same hash, and on the server resolve bringing in video format... you can return the same file, binary...

  • The browser compares the timestamp obtained in view.php with that obtained in load_video.php. Forgive the ignorance, but the time elapsed between getting the timestamp on view.php and obtaining the timestamp obtained in load.php is less than one second ?

  • 1

    You have to find an asynchronous way to do that, but it’s just an idea to help you think of a strategy for your problem...

  • @Ivanferrer, I imagined. I thought of storing a token in $_SESSION and replace it each time leitor.php complete your work. In the case of your example, when you do a get for leitor.php the user could view the value of the var video (&video=L3ZpZGVvL3ZpZGVvX25vbWVfMi5tcDQ=) ?

  • 1

    There are other ways to encrypt this..., using password_hash() or openssl_encrypt(), for example, see a how to use here.

  • 1

    I made a example in the ideone for you to see how to do this.

  • @Ivanferrer, thank you for your time. I think that at some point the browser should receive the decrypted URL so that it can perform the request to the video. In case who would send the decrypted URL to the browser would be leitor.php. I am correct?

  • Yes, the URL has to be decrypted in order to be recognized by the reader, but not necessarily exposed... through a link visually speaking, just as a pointer, you can receive a php file that will return as if the video address output was itself... the video can be rendered through the request and not its exact path, and this video file, can be a video player, not the video itself... a video within another... see this link

Show 6 more comments

Browser other questions tagged

You are not signed in. Login or sign up in order to post.