Alexander Gromnitsky's Blog

Obfuscating Image Links

Latest update:

I noticed this recently, though it started happening about a year ago. On some websites (archive.org's bookreader), a normal <img> tag suddenly began to look like some insane MS Internet Explorer extension from 1998:

<img src="blob:https://example.com/b501e863-fe43-4b63-ae5d-dac14cac097e">

The web page that contains it renders the image fine, but when a fairly naïve user posts that link into a chat, it results in nothing: the blob referenced by <img> exists only in the memory of a specific browser instance, & the server will return 404 for any https://example.com/UUID.

Why do they do this? Every n years some people get scared of hotlinking (bastards keep stealing our traffic), AI luddites try to ruin business of evil corporations, & fans of toy-level DRM amuse themselves with a new scheme.

If you look at the fetch-requests of such a page, you'll see resources that look like images but actually aren't:

$ url='https://ia800206.us.archive.org/🙈.jpg'

$ curl -sI "$url" | grep -e type -e length -e obfuscate
content-type: image/jpeg
content-length: 267470
x-obfuscate: 1|uoEV6/PZOWtGhOZdVM898w==

$ curl -s "$url" | head -c25 | file -
/dev/stdin: data

Such a .jpg is encrypted. Part of the key is in the X-Obfuscate header, but neither a random AI scraper nor any social network knows about this. The cipher is also not disclosed, & every website may use any scheme it wants: following any recommendations would defeat the entire purpose of obfuscation.

The image-rendering algorithm then becomes:

  1. download the encrypted file;

  2. decrypt it using the key from the corresponding header & push the result into a blob;

  3. create a link to the blob using URL.createObjectURL function;

  4. inject into the DOM an img element whose src attribute is equal to the newly created link.

We can increase entropy further by writing our own custom element:

<img-blob alt="a fluffy cat" src="cat.bin"></img-blob>

that will do all of the above on its own. (I terser'ed the source code of the example for I absolutely don't want you to use it in anything serious: the entire approach is extremely user-hostile & anti-web.)

For efficiency, archive.org AES-CTR-encrypts only the first 1024 bytes of the image. Browsers know about AES but strictly require a secure context that can be annoying during testing; hence, for mickey mouse DRM we can simply use XOR-encryption.

The X-Obfuscate header itself can be obfuscated even more, e.g.:

x-obfuscate: rlW2MKWmnJ9hVwbtZFjtVzgyrFV6VPVkZwZ0AFW9Pt==

looks like a base64 string, but:

$ echo rlW2MKWmnJ9hVwbtZFjtVzgyrFV6VPVkZwZ0AFW9Pt== | base64 -d | xxd
base64: invalid input
00000000: ae55 b630 a5a6 9c9f 6157 06ed 6458 ed57  .U.0....aW..dX.W
00000010: 3832 ac55 7a54 f564 6706 7400 55bd 3e    82.UzT.dg.t.U.>

I checked if DeepSeek could figure it out: it spent 9 minutes & left 2 villages in Zhejiang province without water, was several times very close to the target, but ultimately failed.

The string had been post-processed with rot13:

$ alias rot13="tr 'A-Za-z' 'N-ZA-Mn-za-m'"
$ echo rlW2MKWmnJ9hVwbtZFjtVzgyrFV6VPVkZwZ0AFW9Pt== | rot13 | base64 -d
{"version": 1, "key": "12345"}

As homework, you can add an equivalent of loading="lazy" to the custom element using the Intersection Observer API.


Tags: ойті
Authors: ag