Restrict access to WordPress media files to logged in users using ACF

Explore how to restrict access to wordpress media files using htaccess rules and php to disallow unathorized viewing of wordpress attachments

Why should you restrict access to WordPress media files

If you are running a WordPress website with content that is password protected or accessed via logged in users, the media / attachments that are embedded via these protected pages could still be accessed by anyone who has direct links to those files, this is because you are only restricting access to the WordPress content and not the uploaded file.

How to protect WordPress uploads

This article will take you through the steps needed to protect your WordPress media files, by redirecting all media file urls through a custom php script to check if the current user has the required access level and only then will the file display, otherwise it will return a 403 forbidden header to unauthorized viewers.

Add metabox to display File Access permission custom fields using ACF

Using Advanced Custom Fields we can simply add an extra field onto the WordPress attachments form, allowing us to restrict access on a per attachment basis to either public or logged in users, custom field labelled “Access Level” shown in the screenshot below.

To start with make sure you have the Advanced custom fields plugin installed, and import the following json file via Custom Fields > Tools > Import Field Group. This will add a dropdown onto the attachment form where you can choose the attachments access level.

[
    {
        "key": "group_5eb92dc3d1eb5",
        "title": "Attachment Access",
        "fields": [
            {
                "key": "field_5eb92dfc0e5e5",
                "label": "Access Level",
                "name": "access_level",
                "type": "select",
                "instructions": "",
                "required": 0,
                "conditional_logic": 0,
                "wrapper": {
                    "width": "",
                    "class": "",
                    "id": ""
                },
                "choices": {
                    "public": "Public",
                    "member": "Members Only"
                },
                "default_value": [
                    "public"
                ],
                "allow_null": 0,
                "multiple": 0,
                "ui": 0,
                "return_format": "value",
                "ajax": 0,
                "placeholder": ""
            }
        ],
        "location": [
            [
                {
                    "param": "attachment",
                    "operator": "==",
                    "value": "all"
                }
            ]
        ],
        "menu_order": 0,
        "position": "normal",
        "style": "default",
        "label_placement": "top",
        "instruction_placement": "label",
        "hide_on_screen": "",
        "active": true,
        "description": ""
    }
]

Check file access level before displaying WordPress media files

With the metabox added allowing you to choose the access level we now need to add the php script that is responsible for checking the users access level, create a new php file called resource.php in your wordpress installs webroot.

<?php

function get_attachment_id_from_resource_url($request_uri)
{
    // TODO: Get Attachment id from resource url
}


function render_resource($filepath)
{
    // TODO: Add no cache header and display file
}

function init_resource()
{
    // TODO: Check to see correct access level
}

init_resource();

Our resource.php file is made up of the following 3 functions. init_resource which is in charge of capturing the current file request and displays the correct response of either successfuly displaying the WordPress media file, displaying a 404 file not found, or a 403 Access forbidden to users without the correct access level.

function init_resource()
{
    $request_uri = $_SERVER['REQUEST_URI'];
    $filepath = __DIR__ . $request_uri;

    // if file doesnt continue loading wordpress
    if (!file_exists($filepath)) {
        exit;
    }

    // Load wordpress core
    define('WP_USE_THEMES', false);
    require(dirname(__FILE__) . '/wp-blog-header.php');

    // No attachment id, escape
    $id = get_attachment_id_from_resource_url($request_uri);
    if (!$id) {
        header("HTTP/1.1 404 Not Found");
        exit;
    }

    // Check to see correct access level
    $access_level = get_post_meta($id, 'access_level', true);
    if ($access_level === 'member' && !is_user_logged_in()) {
        header("HTTP/1.1 403 Unauthorized");
        exit;
    }

    render_resource($filepath);
}

Get attachment id from WordPress media file url

get_attachment_id_from_resource_url is used to convert the attachment file url into a post object id, this is to confirm that the attachment is for a real file request, Due to the way you WordPress has multiple image sizes per image we need to take into account images suffixed with resize dimensions e.g. image-100×100.png

function get_attachment_id_from_resource_url($request_uri)
{
    // Get Attachment id from resource url

    $url = esc_url_raw($request_uri);
    $id = attachment_url_to_postid(site_url($url));

    // handle attachment image sizes 100x100.png
    if (!$id && preg_match('/(.+)-\d+x\d+(\.\S+)$/', $url, $matches) === 1) {
        $url = $matches[1] . $matches[2];
        $id = attachment_url_to_postid(site_url($url));
    }

    return $id;
}

Output WordPress media file with no cache headers

render_resource displays the WordPress media file along with no-cache headers, to make sure the users browser rechecks the file protection so that it is able to restrict access to wordpress media files each time.

function render_resource($filepath)
{
    // Add no cache header and display file

    // no-cache headers
    header("HTTP/1.1 200 OK");
    header("Expires: 0");
    header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
    header("Cache-Control: no-store, no-cache, must-revalidate");
    header("Cache-Control: post-check=0, pre-check=0", false);
    header("Pragma: no-cache");

    // output image
    header("Content-Type: " . mime_content_type($filepath));
    header("Content-Length: " . filesize($filepath));
    echo file_get_contents($filepath);
}

Add HTACCESS rule to protect WordPress uploads

With our resource.php file ready to capture and protect wordpress uploads we need to capture the request which can be done via Apache htacees file.

The HTACCESS file captures the request of all png, jpg, pdf and docx and redirects the request through our new resource.php file.

# BEGIN ATTACHMENT_RESTRICTION

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^resource\.php$ - [L]
RewriteCond %{REQUEST_URI} ^/wp-content/uploads/
RewriteCond %{REQUEST_URI} (\.png|\.jpg|\.jpeg|\.pdf|\.docx)$ [NC]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /resource.php [L]
</IfModule>

# END ATTACHMENT_RESTRICTION

Conclusion

This was a breif introduction into restricting access to WordPress media files and because of this resource.php was added into the webroot, i would recommend if used on a live website that you move this resource.php into your plugin or theme folder and update the htaccess file accordingly.

This article is a good base to extend upon for example restricting access to WordPress media files by user role, or converting it into a plugin you can automatically append the redirect rules to htaccess.

Leave a Reply

Fields marked with an * are required to post a comment