2019-07-13 20:25 — By Erik van Eykelen
I always try to keep the number of gems I use in projects as small as possible. If you’re not careful you end up adding tens of thousands of lines of code that you don’t know, that could harbor strange side effects–or worse–introduce security flaws.
This article by Thoughtbot puts it well:
Adding another gem is adding liability for code I did not write, and which I do not maintain.
The article makes other good cases to think twice before adding yet another gem to your project.
Recently I was facing the decision whether or not to add the Cloudinary gem to a project. Because I only needed to create signed URLs and to compute upload signatures I decided to write the necessary code myself.
Signed URLs
Signed URLs prevent tampering with URL parameters. For instance, suppose you display small photos on a thumbnail gallery, with a thumbnail URL looking like this:
https://res.cloudinary.com/example/image/upload/s--01Pkxgsb--/c_crop,f_jpg,w_240,h_240/v1/artworks/eQzy...c4vk
You don’t want to enable downloading full-size images simply by manipulating parameters:
https://res.cloudinary.com/example/image/upload/s--01Pkxgsb--/c_crop,f_jpg,w_2000,h_2000/v1/artworks/eQzy...c4vk
Signed URLs prevent this kind of tampering. The code to create a signed URL looks like this:
def signed_url(public_id:, transformations:)
to_sign = ([transformations, "v1", public_id]).join("/")
secret = ENV.fetch('CLOUDINARY_API_SECRET')
signature = 's--' + Base64.urlsafe_encode64(Digest::SHA1.digest(to_sign + secret))[0,8] + '--'
"https://res.cloudinary.com/#{ENV.fetch('CLOUDINARY_CLOUD_NAME')}/image/upload/" + [signature, to_sign].join("/")
end
I obtained the signature magic from this article.
Compute upload signatures
The upload signature must be passed along when uploading a file to the Cloudinary API. It’s a hash of the file name (called public ID), timestamp, folder name, and API secret.
The following comes straight from one of my Rails controllers:
def signature
folder = ENV.fetch('CLOUDINARY_FOLDER')
public_id = SecureRandom.urlsafe_base64(32)
timestamp = Time.now.utc.to_i # Cloudinary expects UTC epoch
payload_to_sign = "folder=#{ENV.fetch('CLOUDINARY_FOLDER')}"
payload_to_sign << "&public_id=#{public_id}"
payload_to_sign << "×tamp=#{timestamp}"
signature = Digest::SHA1.hexdigest(payload_to_sign + ENV.fetch('CLOUDINARY_API_SECRET'))
render(json:
{
api_key: ENV.fetch('CLOUDINARY_API_KEY'),
signature: signature,
folder: folder,
public_id: public_id,
timestamp: timestamp
})
end
By writing two small pieces of code (plus two tests) I’ve eliminated the need for an extra gem which would have added even more gems as dependencies (aws_cf_signer
, domain_name
, http-cookie
, mime-types
, mime-types-data
, netrc
, rest-client
, unf
, and unf_ext
)!
It just feels cleaner to carry less bagage around.