Upcoming Course: Code Your Own Business w/ React + GraphQL!
We're live-coding on Twitch! Join us!
Securing website images the Instagram way using ImageKit

Securing website images the Instagram way using ImageKit

All of us, invariably, use images on our websites and apps. We put in a lot of effort, time & money in getting these images - either from a professional photographer or from our users. They are a precious asset for us. Unfortunately, it is common to have our websites and images scraped by rogue bots, other third parties or our competitors. They take away these images and start using them on their website or misuse them. In this tutorial, we look at how an app like Instagram restricts access to its image URLs. We build upon that to find simple ways in which we can secure the images on our website and discourage their unwarranted use.

How does Instagram secure its images?

Instagram is one of the largest platforms for user-generated images. We picked up an image URL (using the "Inspect Element" method) and tried modifying it.

We can see that

  1. The image size mentioned in the URL cannot be modified
  2. Any other part of the URL cannot be modified or removed. It either results in an incorrect URL hash or an incorrect URL timestamp error

We can interpret from the above example that apps like Instagram protect their URLs against modifications. They also set an expiry time for their URLs. After we cross that expiry time, a request to this URL starts returning an error.

We replicate the same behaviour in this tutorial.

What are we building in this tutorial?

We implement some features similar to what we saw with the Instagram URL and also add some security features used by stock photography websites.

  1. Build a small image gallery similar to a stock photography website.
  2. Implement some image security techniques as we saw above with Instagram URLs
  3. Watermarking low-res images to prevent any misuse
  4. Providing restricted access to the original high-res image to only paid users

What do we need for this demo?

  1. Basic HTML skills
  2. Basic backend skills (we are going to use NodeJS in the backend, but can be any other language as well)
  3. A free account of ImageKit.io. ImageKit.io is a complete image optimization and management solution. It comes with a bunch of transformations like resizing, cropping and watermarking, along with the image security features which make it simple for us to implement such functionality for our images.

Getting the basic setup

To create a food photo gallery for our demo, we download three photos from pexels.com.

Essential Reading: Learn React from Scratch! (2019 Edition)

1. Get image URLs for your images

We upload the images that we downloaded to the media library that comes with ImageKit.io. The media library provides an easy-to-use interface for managing our assets, and we get a URL for every image that is uploaded there.

For example, here is one URL of an image that we uploaded to our ImageKit.io media library for this demo

https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg

2. Set up the basic HTML

We create a simple layout, as shown below, with the three images resized and placed side-by-side using the img tag.

Let's resize our images to 300px by 300px. ImageKit.io comes in handy here as well. We can use the URL-based, real-time transformation parameters to transform our image to any size (or any other variation) that we want.

Here is our image tag with the first image resized to 300x300. Note the resizing parameters at the end of the URL.

<img src="https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg?tr=w-300,h-300" />

Let's take this a step further and also cater to high-density screens. Using the srcset attribute of the img tag along with the dpr transformation of ImageKit.io, allows us to adapt our image to any DPR value.

With this change, our HTML now looks like this -

<html>
<head>
    <title>My Food Gallery Page</title>
</head>
<body>
    <h1>My Food Gallery</h1>
    <img 
        src="https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg?tr=w-300,h-300" 
        srcset="https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg?tr=w-300,h-300, 
            https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg?tr=w-300,h-300,dpr-2 2x" 
    />
    <img 
        src="https://ik.imagekit.io/demo/img/scotch_security/2_fGwKLAgUUd.jpeg?tr=w-300,h-300"
        srcset="https://ik.imagekit.io/demo/img/scotch_security/2_fGwKLAgUUd.jpeg?tr=w-300,h-300, 
            https://ik.imagekit.io/demo/img/scotch_security/2_fGwKLAgUUd.jpeg?tr=w-300,h-300,dpr-2 2x" 
    />
    <img 
        src="https://ik.imagekit.io/demo/img/scotch_security/3_xqMQs0ib3.jpeg?tr=w-300,h-300" 
        srcset="https://ik.imagekit.io/demo/img/scotch_security/3_xqMQs0ib3.jpeg?tr=w-300,h-300, 
            https://ik.imagekit.io/demo/img/scotch_security/3_xqMQs0ib3.jpeg?tr=w-300,h-300,dpr-2 2x" 
    />
</body>
</html>

We are ready with our basic food gallery to start implementing image security.

Securing our images

With the basic setup done, we now get started with securing our images

1. Watermarking the images

One of the ways of securing our images is to watermark them. Watermarking is a common practice adopted by many websites which own the rights to the images on their website like by stock photo galleries.

Some of us might not want to use the watermarking method, in which case, we can skip to the next step.

For watermarking, we have uploaded a logo to ImageKit's media library.

This logo is accessible at

https://ik.imagekit.io/demo/img/scotch_security/secure_cr21l_7GU.png

To watermark our images, with this logo, we use ImageKit's overlay transformation and place the resized watermark of width 150px on the bottom right corner of our image. The parameters used for this are pretty evident from the example below. All of this happens in real-time. So, now our image URL looks like

<img src="https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg
        ?tr=w-300,h-300
        ,oi-@@scotch_security@@secure_cr21l_7GU.png
        ,ofo-bottom_right
        ,ow-150" />

Also, our food image now looks like

Adding a watermark ensures that nobody can pick up our thumbnail and start using it on their website.

2. Preventing URL change or removal of the watermark

In whatever examples we have seen till now, all the transformations have been applied directly to the URL. While this real-time transformation is fantastic as it allows us to fetch any transformation that we want to, it has a down-side that someone can remove the transformation string altogether and get the original image.

In our case, someone can look at the URL, figure out what parameters correspond to the overlay and remove them. Same applies for any other parameter in the URL.

This is not what we want for our demo. Nobody should be able to modify our URL.

Part a. Generating a signed URL

Here, we use another technique, similar to the one we saw with the Instagram URL, that prevents any unwanted modification to the URL.

We sign the URL.

A signature is a hash of any text, generated using a hashing algorithm like SHA1 and a private key that is known only to the client and the server. In our case, the client is us, and the image server is ImageKit.

We generate a hash of our image URL (the text). We pass this generated signature with the rest of the URL to the image server. The server then creates a signature of the entire URL using the same private key and compares it to the signature received from the client. If the two signatures match, the request gets served. If the two signatures do not match, it means either the URL or the signature got tampered. Thus, the request gets blocked. This behavior has been implemented in the "Part b." below.

When using ImageKit, generating a signed URL is quite simple, using one of the SDKs provided. In our example below, we are using the NodeJS SDK.

Note that, signature generation happens on the backend because the private key should not be exposed to the outside world.

/__
    To be done on our NodeJS server
__/
var imagekit = new ImageKit({
    privateKey : "our_private_key", //this is the main parameter needed for signature generation
    ...
});

var signedURL = imagekit.url({
    src : "https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg?tr=w-300,h-300,oi-@@scotch_security@@secure_cr21l_7GU.png,ofo-bottom_right,ow-150",
    signed : true   //this parameter helps in generating a signed URL
});

The signed URL generated using the SDK looks like this -

https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg?tr=w-300,h-300,oi-@@scotch_security@@secure_cr21l_7GU.png,ofo-bottom_right,ow-150&ik-s=b19a8d348746e1e7f89a8c0262507ce36ad41a3d

Note that there is an additional parameter ik-s at the end of the URL.

In our case, for simplicity, we generate the signature for the URL and replace the existing URL in the HTML with the new signed URL. In real-life scenarios, the server uses a template to generate the HTML. So, it is the server that generates the signed URLs and uses them in the img_ tags in the generated HTML._

Part b. Preventing any unsigned image URL from getting accessed

Just creating a signed image URL is not enough. We also need to make sure that the image server, in this case, ImageKit, does not respond to any image request that does not have a valid signature attached to the request.

ImageKit makes it simple. Our ImageKit account comes with a setting which blocks any unsigned requests. This fulfils our requirement. We need to turn on this setting, as shown below.

The result after signature generation

Here is how our HTML looks now with the new signed URLs for 300x300 watermarked images.

The URLs below are only for demonstration purposes. The actual impact of tampering a correct signed URL can be seen in the GIF that follows this code.

<html>
<head>
    <title>My Food Gallery Page</title>
</head>
<body>
    <h1>My Food Gallery</h1>
    <img 
        src="https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg?tr=w-300,h-300,oi-@@scotch_security@@secure_cr21l_7GU.png,ofo-bottom_right,ow-150&ik-s=7cf746087e01ed1d3ad58ca64156745070e69e3e" 

        srcset="https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg?tr=w-300,h-300,oi-@@scotch_security@@secure_cr21l_7GU.png,ofo-bottom_right,ow-150&ik-s=7cf746087e01ed1d3ad58ca64156745070e69e3e,

            https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg?tr=w-300,h-300,dpr-2,oi-@@scotch_security@@secure_cr21l_7GU.png,ofo-bottom_right,ow-150&ik-s=02bd8e66a0000dbdb73619211743c8f6fa5addf0 2x" 
    />
    <img 
        src="https://ik.imagekit.io/demo/img/scotch_security/2_fGwKLAgUUd.jpeg?tr=w-300,h-300,oi-@@scotch_security@@secure_cr21l_7GU.png,ofo-bottom_right,ow-150&ik-s=1bbab2b2fc63abdaf585197b1d9707220cc07b6c"

        srcset="https://ik.imagekit.io/demo/img/scotch_security/2_fGwKLAgUUd.jpeg?tr=w-300,h-300,oi-@@scotch_security@@secure_cr21l_7GU.png,ofo-bottom_right,ow-150&ik-s=1bbab2b2fc63abdaf585197b1d9707220cc07b6c, 

            https://ik.imagekit.io/demo/img/scotch_security/2_fGwKLAgUUd.jpeg?tr=w-300,h-300,dpr-2,oi-@@scotch_security@@secure_cr21l_7GU.png,ofo-bottom_right,ow-150&ik-s=9030030923deada4531f619e033660ba510499d6 2x" 
    />
    <img 
        src="https://ik.imagekit.io/demo/img/scotch_security/3_xqMQs0ib3.jpeg?tr=w-300,h-300,oi-@@scotch_security@@secure_cr21l_7GU.png,ofo-bottom_right,ow-150&ik-s=ec65c1f6354de6bb4e16d2ae3bb13b3828f9816e" 

        srcset="https://ik.imagekit.io/demo/img/scotch_security/3_xqMQs0ib3.jpeg?tr=w-300,h-300,oi-@@scotch_security@@secure_cr21l_7GU.png,ofo-bottom_right,ow-150&ik-s=ec65c1f6354de6bb4e16d2ae3bb13b3828f9816e, 

            https://ik.imagekit.io/demo/img/scotch_security/3_xqMQs0ib3.jpeg?tr=w-300,h-300,dpr-2,oi-@@scotch_security@@secure_cr21l_7GU.png,ofo-bottom_right,ow-150&ik-s=b71bc7d5e9d0e11b8af5c1250ec50ae546e165ad 2x" 
    />
</body>
</html>

Tampering a signed URL now results in an error. See the GIF below. Similar to what we saw with Instagram

So now, no one can remove the watermark from our image or get any other size except for the 300x300 size used in the above URL.

Providing access to the original image to paid users

With the listing images secure, the next step of our food gallery is to provide access to the original image to our paid users for a limited time.

At this stage of the tutorial, it is not possible to get the original image from the URL, because our ImageKit account is set to deliver only those images that have a signature attached.

We need to follow the same step that we did in the last section to generate a signed URL even for our original image.

Plus, we add another thing to the mix. We want to make sure that the URL expires in 1 hour. So, we pass another parameter in our code to generate the signed URL providing the number of seconds before the URL expires.

var signedURL = imagekit.url({
    src : "https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg?tr=w-300,h-300",
    signed : true   //this parameter helps in generating a signed URL
    expireSeconds : 3600 //number of seconds in which the URL should expire
});

This gives a URL like

https://ik.imagekit.io/demo/img/scotch_security/1_GTMtFxEf5.jpeg
    ?tr=w-300,h-300
    &ik-s=f65fd8e3136b557e61dc69c2ed19182f07a1ce5
    &ik-t=1234567890

Note that the above URL now has an additional query parameter ik-t which is basically the timestamp after which the URL should not work. If we want to keep the image accessible beyond the 1-hour mark, we have to generate a new URL with a new expiry time and the associated signature.

Making the signature setting image-specific instead of account-wide

In the tutorial, we changed our ImageKit account's setting to prevent it from delivering any image against an unsigned image URL. This was an account-wide change. However, there might be a case where we are also using ImageKit for some other images that do not need a signature. Like a logo on our website or an icon.

If that's the case, then in ImageKit there is a feature to make this restriction on unsigned URLs, specific to a particular image instead of applying it to the entire account.

This feature is called "Private Images". We can mark an image as "private" when we upload it to ImageKit's media library. A private image can only be accessed via signed URLs even if the account level setting of using signed URLs is turned off. You can secure select images this way, leaving the others for regular use.

Conclusion

Using image security features is essential if we want to prevent misuse of our image URLs.

Also, it is true, not just for the photo gallery we just built, but for many other cases. While it will not completely stop the bots from hitting our website, but it makes it all the more tough for them to use our images anywhere.

ImageKit makes it really simple for us to implement these security features instead of having to work them on our own.

Apps like Instagram are already using such security measures for their image URLs. Let's implement it for our images using ImageKit.

This content is sponsored via Syndicate Ads.