Restricting Access to Resources

The Usual Situation

Many open-source PHP platforms like Wordpress handle user-submitted content very straightforwardly. If you upload a file, it typically gets saved in some publicly accessible URL on your site, such as https://example.com/wp-uploads/.../my-file.png – so anyone can load it and view it.

While that may be desirable for images on a public blog, we here are Qbix are building community apps that allow people to manage and collaborate on content, and some of it can be private, accessible to specific users, or roles in a community. In fact, Qbix has a very robust system of access control with different levels for read, write and admin, as well as support for custom roles and permissions on a very granular level.

Access Control for Data

Clients make an HTTP GET web request to a web server like NGINX, which in turn forwards the request to the PHP application using proxy_pass or something similar. The PHP application then looks into the database (e.g. MySQL) and finds the data. It then calculates the access control (e.g. using Streams::calculateAccess($asUserId, $publisherId, $streams)) and applies the rules before returning the data. A similar procedure is done for PUT, POST, DELETE and other HTTP methods when modifying a resource. Here is a broader view of how a typical Qbix server setup processes requests:

The response is typically some HTML or JSON containing the data requested, after applying the access rules. Thus, for example, rendering an avatar may return only the firstName but not the lastName. Visiting a user’s profile and clicking Add to Contacts may generate a vCard with their emailAddress and phoneNumber, or it may not, depending on whether they have chosen to share this information with the currently logged-in user.

Access Control for Files

Files are a bit different, because they are stored in the file system. One could, of course, store files in a MySQL database as BLOBs, and then have the PHP app try to stream the information from MySQL to the webserver, and from there to the browser. Perhaps storing files in MySQL databases instead of a filesystem like ext4 might even lead to more efficient lookups. However, this needlessly ties up the application into doing a lot of file I/O. In the case of PHP, it’s even worse because PHP with the typical fastcgi process manager (FPM) setup just spawns a limited number of preforked threads that can only handle one request at a time. So every thread that’s spending time piping data from MySQL to NGINX or Apache is a thread that can’t be handling new web requests.

There is a second approach, however, that involves the PHP code doing what it’s designed to do: checking the access control, and then returning an “internal redirect” header to the web server, telling it where to find the file, or whether to serve some generic content with an 40x series HTTP code:

  • 401 - Not Authorized - please log in
  • 402 - Payment Required - of some kind
  • 403 - Forbidden - not enough privileges
  • 404 - Not Found

or whether to serve the actual content with a 200 Success result. In the latter case, the PHP server would tell NGINX where to find the file to serve. Like this:

header('X-Accel-Redirect: /srv/hidden-files/` . $path);

Set up NGINX configuration file

You can read more about internal redirects in NGINXhttps://shortcut.com/developer-how-to/how-to-use-internal-redirects-in-nginx, here is an example:

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        server_name _;

        location / {
                proxy_pass http://127.0.0.1:8000/;
        }

        location ~ \.php$ {
                fastcgi_param MOD_X_ACCEL_REDIRECT_ENABLED on;
        }


        location /hidden-files/ {
                internal; # This tells nginx it's not accessible from the outside
                alias /srv/hidden-files/;
        }
}

Alternative Approach

Qbix is designed to work with many environments, not just NGINX. So if it uses a different web server which doesn’t even support internal redirects, as a fallback approach, the platform could try to store the uploaded file inside a folder with a long and unguessable name, inside the usual uploads location, like this:

MyApp/web/Q/uploads/abchjkerkjervkjwnvjkr/uploaded-file.png

This location, though, would never be revealed by PHP to browsers. Instead, if a user is authorized to access the file, Qbix Platform would call Q_Utils::symlink() to create a temporary symlink in a folder with a name like. So for example:

MyApp/web/Q/uploads/1669355665/kerhvjrpvekrnvqa/uploaded-file.png
                 {{timestamp}}/{{randomString}}/{{filename}}

Then PHP outputs something like this, so the browser will load that file:

Q_Html::img('Q/uploads/1669355665/kerhvjrpvekrnvqa/uploaded-file.png');

Finally, a periodic cron job would run another PHP script that would go through Q/uploads and remove all timestamps which are earlier than 5 minutes ago. One hour is plenty for a turnaround time from the browser.

This alternative approach does have a weakness that the user could theoretically right-click and share the image with someone else within that 5 minute window that it’s still available. But it’s still more secure than leaving that file there indefinitely, allowing any recipients of the URL as much time as they want to access it also. That’s how sensitive data can fall into the wrong hands.