Hide a React App Inside a JPEG (and run it)

Andrei Gaspar
Sopra Steria Norge
Published in
6 min readAug 7, 2023
Article Cover Image by Author

In today’s episode of aimlessly adventurous, we code first and ask questions later. With the current surge in image AIs, you’ve likely seen a million ways to squeeze data inside images — but the method that we’re exploring below doesn’t involve AI, can theoretically store any amount of data, and doesn’t change the value of a single pixel in the image.

If you want to hide a secret in plain sight, store your crypto keys in your profile picture, or run an entire app from within a JPEG, this guide should have you covered.

So, let’s see how we can make a React app — that can hide React apps inside a JPEG — run from inside a JPEG.

I’ve uploaded the app to GitHub Pages; feel free to play around with it.

Also, as you can see in the app’s repository, the app runs from inside a JPEG, and the entire HTML body consists of the following code snippet.

<body>
<img src="./react-inside.jpeg" />
</body>

The entire application source code is available on GitHub, so instead of copy-pasting the solution, I’m going to be discussing the approach.

How Does It Work?

It is simpler than it might seem at first glance. Since we’re not encoding information into the pixels themselves, we don’t need any fancy algorithms. What we are leveraging instead are the markers of the JPEG format.

You can easily examine the hex values of a JPEG file with GHex (Linux) or any other hex editor for the operating system of your choice.

JPEG files start with a marker that contains the hex values FF D8 FF.

GHex Screencap — the start marker (Image by Author)

JPEG files also lack any kind of information about the length of the content embedded in them. They rely on an end marker, which is always FF D9.

GHex Screencap — the end marker (Image by Author)

If the JPEG would be an HTML file, then FFD8FF would be the equivalent of the opening tag <html>, and FFD9 would be the closing tag </html>.

Now that we know that this is the protocol and systems rely on it to display the JPEG image, we can simply append our data after the closing marker, and nobody will be the wiser.

We can append any kind of data as long as we make sure to convert it to hex first.

How to Get This Done in the Browser?

This is something that would be way easier on the backend, but as long as we’re willing to jump over a couple of hoops, doing it in the browser isn’t impossible, either.

So, let’s break this down into a few steps:

  • Get the Hex value of a JPEG.
  • Append to the Hex value or read from it.
  • Create JPEG from a Hex value.

Get the Hex Value of a JPEG

Getting the Hex value on the backend with Node would be just a couple of lines of code.

// We can't do this in the browser, unfortunately.
const fs = require('fs')

fs.readFile('your_image.jpg', (err, data) => {
if (err) throw err
const hex = data.toString('hex')
console.log(hex)
})

In the browser, however, you’ll need some extra steps.

First off, we need to get the raw binary data called ArrayBuffer from our image.

The simplest solution I found is using fetch and calling the arrayBuffer() helper method on the result.

await (await fetch("YOUR_IMAGE_SRC/URL")).arrayBuffer()

Now that we have the ArrayBuffer, we’ll need to convert it to a Uint8Array, because the ArrayBuffer cannot be directly worked with.

Each value in the Uint8Array represents a byte in our file, and converting each byte to hexadecimal will give us the hex value of the file.

const bytes = new Uint8Array(arrayBuffer)

Below you can see a helper function that does exactly that.

bytesToHex(bytes) {
const hex = []

for (let i = 0; i < bytes.length; i++) {
const current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i]
hex.push((current >>> 4).toString(16))
hex.push((current & 0xf).toString(16))
}

return hex.join('')
}

Append to the Hex Value or Read from It

Once we have our hex result, we can append our data to it or read from it.

If we want to append, we need first to transform our data to hex and then concatenate it to the end of the hex result we got from the clean JPEG.

Below you can see a helper function that transforms strings to hex, helping us to hide arbitrary strings in the image.

stringToHex(str) {
let hex = ''

for (let i = 0; i < str.length; i++) {
hex += '' + str.charCodeAt(i).toString(16)
}

return hex
}

Once you transformed your data into hex, you just need to append it to the original, and you’re good to go.

In the case where you’d like to read from it, all you need to do is split the string on the end marker ffd9 and convert the second part of the hex back to a string.

Hex string split at ffd9 marker

Below you can see a function that transforms hex to string.

stringFromHex(hex) {
let str = ''

for (let i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16))
}

return str
}

Create a JPEG from a Hex Value

If we manage to slap our own hex at the end of the image hex, the last step is to create a JPEG out of it. Just like when getting the hex, we’ll need to do some transformations first.

Here’s a helper function that converts hex to bytes.

hexToBytes(hex) {
const bytes = []

for (let i = 0; i < hex.length; i += 2) {
bytes.push(parseInt(hex.substr(i, 2), 16))
}

return bytes
}

Don’t forget the Uint8Array conversion again!

const buffer = new Uint8Array(bytes)

Once we have the data, you have a couple of different ways of transforming it into a JPEG in the browser.

For example, you can create an image URL.

bufferToImgUrlSrc(buffer) {
const blob = new Blob([imageBuffer], { type: 'image/jpeg' });
const urlCreator = window.URL || window.webkitURL;
const imageUrl = urlCreator.createObjectURL(blob);

return imageUrl;
}

Or, you can also transform it into base64 and insert it into an image src.

bufferToBase64Src(buffer) {
const buffer = new Uint8Array(buffer)

const b64encoded = btoa(
buffer.reduce((data, byte) => data + String.fromCharCode(byte), '')
);

return `data:image/jpeg;base64,${b64encoded}`
}

To Wrap Things Up

Now that we covered the essential parts, with a glance at the source code, you’ll be able to see how a React app — that can hide React apps inside a JPEG — can itself run from inside a JPEG.

Here’s a summary of the step-by-step process:

  • Created a React app using the create-react-app package.
  • Implemented the JPEG helper functions in a JpegClient.
  • Implemented the UI in React.
  • Used the react-app-rewired package to build the app into a single JavaScript file.
  • Loaded JPEG and injected the content of the build result of the app into the JPEG, using the app.
  • Imported the resulting JPEG into an index.html file.
  • Wrote a tiny script that executes after the DOM content loaded, extracts the custom data injected into the JPEG, and runs eval() on the code.

Admittedly useless to our day-to-day lives, this exercise does shed some light on the workings of the JPEG format, and hopefully, it inspires you to dive into some quirky coding yourself.

Stay curious and happy coding!

--

--