How To Access Images Securely with OAuth 2.0

One of the great benefits of utilizing OAuth 2.0 is that your application totally outsources its authentication responsibility. The result is that the client simply passes an access token as an HTTP request header in each web request to the backend server. The server can then, without the need to hit the identity server, use that token to verify that the user is who he says he is.

For GET and POST requests made with JavaScript, including an additional header with the access token is a trivial task. However, what happens when you need to allow an authorized user to access an image? As a security professional, I get this question quite often.

If you simply add an <img> tag into your HTML document, the user’s web browser will make an anonymous request to the web server for an image. If you have images that should be publicly available, this is perfectly fine. However, if your user is trying to access an image that shouldn’t be accessible by everyone on God’s green Earth, what do you do?

Using a JavaScript client library, you can leverage utility classes to add authentication headers automatically. Thus, if you use Angular, for example, every request made via the Http object can automatically include the access token. The problem with the <img> tag is that the user’s web browser will not issue requests through Angular’s Http object.

How in the world, then, do you get around this problem? Well, I have found four possible solutions, each having their own pros and cons.

1. Utilize Query Parameters

Perhaps the simplest solution is to pass the access token as a query parameter in the <img> tag’s src attribute. For example, you may create an image in your document that looks like:

<img src="api/images/secureimage?access_token=mF_9.B5f-4.1JqM">

According to the OAuth 2.0 specification, this is a perfectly legitimate solution. Once this request reaches the web server, the server will then validate the access token and return an image if the user is authorized.

The pros for this solution are:

  • The OAuth 2.0 specification natively supports this option.
  • This option is easy to implement.
  • The query string is protected by TLS, so the access token is secured in transit.

The cons for this solution are:

  • The access token will be logged in server logs. Anyone with access to the server logs will have access to the access token.
  • The access token will be visible in the browser history. Anyone with access to the user’s browser history will have access to the access token.
  • Depending upon implementation, there is a potential that the query parameter can be passed in referrer headers. Thus, any website that is sent the referrer header will have access to the access token.
  • Every time the access token changes, the URL will change. This will result in cache busting. In other words, the browser will only cache the image for as long as the access token lives.

2. Establish a Session

The next option is to establish a browser session when the user logs into your system. Upon establishment of that session, your web server will then set a cookie, which will be sent by the user’s browser with each subsequent request. Your web server could then bind the user’s session ID to the access token. Thus, you can authorize that the user has access to the image using the session cookie.

The pros for this solution are:

  • There is no information leakage here. The access token will not be logged in the server logs, or the user’s browser history. Nothing is leaked via a referrer header.

The cons for this solution are:

  • You must perform an additional step to establish a session during authentication.
  • Session data will increase the memory footprint on the server and will increase the amount of information passed over the wire.
  • Sessions open new attack vectors, such as cross-site request forgery (CSRF).

3. Use Data URIs

Yet another option is to construct the <img> tag using JavaScript with a data URI. In this case, JavaScript can be used to issue a request to a web API with the access token specified in the header. The web server would then respond with the content of the image. JavaScript will then create a new <img> tag, similar to the following example:

let fetchOptions = {
    method: 'GET',
    headers: {
        'Authorization': 'Bearer mF_9.B5f-4.1JqM'  // you would, of course, not hard-code this here... 
    }
};

//
// returns a json object with two properties:
// - mimeType: the MIME Type of the image (e.g., image/png)
// - base64Content: the binary content of the image, encoded in base-64 format.
//
fetch('/api/images/secureimage', fetchOptions)
    .then(response => response.json())
    .then(response => {
        let image = document.createElement('img');
        image.src = `data:${response.mimeType};base64,${response.base64Content}`;
        document.body.appendChild(image);
    });

The pros for this solution are:

  • This is arguably the most secure solution. There is no information leakage here, not even in referrer headers. And, because no session is required, no additional attack vectors are introduced into the system.
  • There is consistent header usage.
  • The image can be cached (via the server’s response).

The cons for this solution are:

  • This is a much more complex solution.
  • There is a potential size limitation for data URIs, but I can’t find any specifics. The “data” URL schema specification says, “The effect of using long ‘data’ URLs in applications is currently unknown; some software packages may exhibit unreasonable behavior when confronted with data that exceeds its allocated buffer size.”

4. Use Service Workers

This final option may not be quite ready for prime time, but I thought that I’d include it anyways. Service Workers were designed to allow web developers to build applications that handle disconnected scenarios gracefully. Though this is the primary use, you can also use a service worker as a proxy in which you can add the authorization header automatically.

Service Workers are supported in most modern browsers. However, support is somewhat fragmented in mobile browsers.

Register the Service Worker

First, you need to register a service worker when your web page is loading. When you register your service worker, you can designate the scope to be a relative path to where your images are served. The script below registers a service worker script that is in the root of your website and scopes it to ./api/images. This example assumes that when your web application makes a request for an image, it will be fetched via a REST call at https://<domain>/api/images/<image-name>:

if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker
            .register('/service-worker.js', { scope: './api/images/' })
            .then(registration => {
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            })
            .catch(err => {
                console.log('ServiceWorker registration failed: ', err);
            });
    });
}

Intercept the Requests for the Images

Next, in the service-worker.js file, you can intercept all calls made to the scope configured above. The following example will add an authorization header with an access token to the image that is being requested. Note that I had to set the mode to “cors” and credentials to “include” to get the header to propagate.

self.addEventListener('fetch', event => {
    event.respondWith(
        fetch(event.request, {
            mode: 'cors',
            credentials: 'include',
            headers: {
                'Authorization': 'Bearer mF_9.B5f-4.1JqM' // you would, of course, not hard-code this here...
            }
        })
    )
});

Create an Image Tag in HTML

Now, in your web page, all you have to do is add an <img> tag to your page:

<img src="api/images/secureimage.png">

The pros for this solution are:

  • As with the Data URIs solution, there is no information leakage here, and no additional attack vector introduced with session state.
  • There is consistent header usage.
  • The image can be cached.
  • You can author the service worker once and forget about it. You do not have to dynamically emit HTML, as long as the service worker is registered before you attempt to load an image.

The cons for this solution are:

  • Service Workers are not supported across all browsers, but there is still good support.
  • This solution can be difficult to debug, and keeping the service worker up to date can be a pain in the butt.
  • Because the solution is so transparent, it may be difficult for someone unfamiliar with the web application to understand the magic behind the scenes.

Conclusion

You should always make security design decisions with respect to the risk of the application. Is the risk of exposing the access token in server logs or via a referrer header acceptable? Choose the simplest option and pass the access token as a query parameter. Need a bulletproof option at the cost of more complexity? Choose Data URIs or Service Workers. Talk with your teammates and your security professionals and choose the most appropriate option for your product. And, whatever choice you make, ensure that you are consistent throughout your implementation.

Leave a Reply