Tech & Engineering Blog

unload And beforeunload Events And How To Easily Debug Them Both!

Gal Weizman

February 6, 2019

Categories: Engineering, Technology and Engineering

unload And beforeunload Events And How To Easily Debug Them Both!

tl;dr — if you’re only here for the trick of how to set a breakpoint and actually debug **unload and beforeunload event listeners, go straight to “The Trick To Successfully Debug unload/ beforeunload Event Listeners” section below.**

As I often do, I was looking for potential vulnerabilities in well known websites and web applications. There is a specific one that I have reasons to believe that might be vulnerable to a serious XSS exploitation.

At some point in the process, I realized that the part that might be vulnerable to XSS is executed by an unload event listener that was registered by some javascript code in the website. This was where I had to debug to be able to tell whether I found something interesting here or not.

So I set a breakpoint where the code that is supposed to be fired by the unload event is, and reloaded the page to allow my Google Chrome Browser stop at this breakpoint of mine letting me debug it.

For those of you who have enough javascript experience, the following will not shock you at all: the browser ignored my breakpoint completely and reloaded the page normally.

That’s kind of annoying. I mean, any javascript code that is intended to execute under this event executes perfectly fine — whether it’s console.log(), localStorage.setItem() or any other standard action. So what’s wrong with my breakpoint? or placing a debugger; command? Why don’t these work?

window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"unload"</span><span class="token punctuation">,</span> <span class="token parameter">e</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"unloading!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// would successfully log to console</span>
  localStorage<span class="token punctuation">.</span><span class="token function">setItem</span><span class="token punctuation">(</span><span class="token string">"key"</span><span class="token punctuation">,</span> <span class="token string">"value"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// would successfully set new item to local storage</span>
  <span class="token keyword">debugger</span><span class="token punctuation">;</span> <span class="token comment">// would fail to break at this point!</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

In order to understand this behavior, it makes sense for us to first understand these events. The purpose of these events is to enable a website to execute pre-configured actions right before it, unloads and when it unloads.
Or, to be more specific, as mentioned in MDN: “The beforeunload event is fired when the window, the document and its resources are about to be unloaded” and “The unload event is fired when the document or a child resource is being unloaded”. (which means it’s not about the website as much as it is about any window that lives within the website, whether it is the top frame or an iframe)

So let’s try to list the different properties of these two events and compare them:

Order: beforeunload will always fire before unload event (makes sense, right?)

<span class="token keyword">const</span> ifr <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"iframe"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>ifr<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// top window's beforeunload event would be the first one to fire</span>
window<span class="token punctuation">.</span><span class="token function-variable function">onbeforeunload</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"first event to fire (1)"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token comment">// other windows beforeunload events would fire after top window</span>
ifr<span class="token punctuation">.</span>contentWindow<span class="token punctuation">.</span><span class="token function-variable function">onbeforeunload</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"second event to fire (2)"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token comment">// top window's unload event would fire after all beforeunload events have been fired</span>
window<span class="token punctuation">.</span><span class="token function-variable function">onunload</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"third event to fire (3)"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token comment">// other windows unload events would fire after top window</span>
ifr<span class="token punctuation">.</span>contentWindow<span class="token punctuation">.</span><span class="token function-variable function">onunload</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"fourth event to fire (4)"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>

Purpose: The beforeunload event triggers right before unloading of the window has begun. unload would trigger while unloading of the window is taking place.

Cancelable: The beforeunload event can be canceled by user interaction:

<span class="token comment">// by https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload#Example</span>
window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"beforeunload"</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  event<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Cancel the event as stated by the standard.</span>
  event<span class="token punctuation">.</span>returnValue <span class="token operator">=</span> <span class="token string">""</span><span class="token punctuation">;</span> <span class="token comment">// Chrome requires returnValue to be set.</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

The above example will trigger a popup asking the user whether they are sure they want to leave the site or not, and in case they choose not to leave, the beforeunload event will be canceled, the unload event will never even initiate, and the page will not reload. On the other hand, that wouldn’t happen for an unload event — once it is fired, there is no way to prevent the window from unloading.

Supporting objects: Both beforeunload and unload events can be registered to be listened to by window, HTMLElementBody and HTMLFrameSetElement

<span class="token keyword">function</span> <span class="token function">successful_listener</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"would successfully fire!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">function</span> <span class="token function">failing_listener</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"would never fire - this message would never be logged to console"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// both ways to register both beforeunload And unload events would work with window</span>
window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"beforeunload"</span><span class="token punctuation">,</span> successful_listener<span class="token punctuation">)</span><span class="token punctuation">;</span>
window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"unload"</span><span class="token punctuation">,</span> successful_listener<span class="token punctuation">)</span><span class="token punctuation">;</span>
window<span class="token punctuation">.</span>onbeforeunload <span class="token operator">=</span> successful_listener<span class="token punctuation">;</span>
window<span class="token punctuation">.</span>onunload <span class="token operator">=</span> successful_listener<span class="token punctuation">;</span>
<span class="token comment">// addEventListener won't work with document.body whereas direct registration would</span>
document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"beforeunload"</span><span class="token punctuation">,</span> failing_listener<span class="token punctuation">)</span><span class="token punctuation">;</span>
document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"unload"</span><span class="token punctuation">,</span> failing_listener<span class="token punctuation">)</span><span class="token punctuation">;</span>
document<span class="token punctuation">.</span>body<span class="token punctuation">.</span>onbeforeunload <span class="token operator">=</span> successful_listener<span class="token punctuation">;</span>
document<span class="token punctuation">.</span>body<span class="token punctuation">.</span>onunload <span class="token operator">=</span> successful_listener<span class="token punctuation">;</span>
<span class="token comment">// addEventListener won't work with frameset whereas direct registration would</span>
<span class="token keyword">const</span> frameset <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"frameset"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>frameset<span class="token punctuation">)</span><span class="token punctuation">;</span>
frameset<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"beforeunload"</span><span class="token punctuation">,</span> failing_listener<span class="token punctuation">)</span><span class="token punctuation">;</span>
frameset<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"unload"</span><span class="token punctuation">,</span> failing_listener<span class="token punctuation">)</span><span class="token punctuation">;</span>
frameset<span class="token punctuation">.</span>onbeforeunload <span class="token operator">=</span> successful_listener<span class="token punctuation">;</span>
frameset<span class="token punctuation">.</span>onunload <span class="token operator">=</span> successful_listener<span class="token punctuation">;</span>

It is worth mentioning that the unload properties of all three (the window, the body and the frameset) point to the same place (same goes for beforeunload):

<span class="token keyword">function</span> <span class="token function">listener</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
<span class="token keyword">const</span> frameset <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"frameset"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> body <span class="token operator">=</span> document<span class="token punctuation">.</span>body<span class="token punctuation">;</span>
window<span class="token punctuation">.</span>onunload <span class="token operator">=</span> listener<span class="token punctuation">;</span>
window<span class="token punctuation">.</span>onbeforeunload <span class="token operator">=</span> listener<span class="token punctuation">;</span>
body<span class="token punctuation">.</span>onunload <span class="token operator">===</span> listener<span class="token punctuation">;</span> <span class="token comment">// true</span>
body<span class="token punctuation">.</span>onbeforeunload <span class="token operator">===</span> listener<span class="token punctuation">;</span> <span class="token comment">// true</span>
frameset<span class="token punctuation">.</span>onunload <span class="token operator">===</span> listener<span class="token punctuation">;</span> <span class="token comment">// true</span>
frameset<span class="token punctuation">.</span>onbeforeunload <span class="token operator">===</span> listener<span class="token punctuation">;</span> <span class="token comment">// true</span>

Oh, and for those of you who wonder “what in the world is a frameset?” — don’t worry about it. It’s this super old and deprecated element that is no longer in use at all. In fact, it is currently defined as obsolete, which means it could be gone in any new version of any major browser any time!

Prototype: Whereas unload is a normal event that inherits from Event, beforeunload has its own prototype, BeforeUnloadEvent. This is in order to implement the beforeunload event’s unique property returnValue, which allows the registered event listener to display the “Are you sure you want to leave this site?” dialog box.

So How Can One Debug A beforeunload/unload Event Listener?

And now for the cool part! As I was saying at the beginning, debugging these events is pretty tricky because, when you think about it, you’re not supposed to be able to. The purpose of the unload and beforeunload events is very clear: Execute a set of preconfigured actions before and when a page unloads. All synchronous actions are guaranteed to fully execute, but when it comes to asynchronous actions that are initiated by synchronous actions inside the event listener, the browser is more like “I’ll do my best to execute all your actions, but when the unloading event finishes I cannot promise you anything”.

window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"unload"</span><span class="token punctuation">,</span> <span class="token parameter">e</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  <span class="token keyword">function</span> <span class="token function">sleep</span><span class="token punctuation">(</span><span class="token parameter">delay</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">const</span> start <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator"><</span> start <span class="token operator">+</span> delay<span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>
  <span class="token comment">// unloading won't finish until 10 full seconds pass!</span>
  <span class="token function">sleep</span><span class="token punctuation">(</span><span class="token number">10000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
    <span class="token comment">// this part would be ignored since unloading will already</span>
    <span class="token comment">// finish by now, thus it won't wait for this async call</span>
    <span class="token function">sleep</span><span class="token punctuation">(</span><span class="token number">10000</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

But does the browser agree to execute any type of action? The answer is no. There are some types of actions that by spec are not allowed to be executed within a beforeunload/unload events listeners. alert() is one example, but the most relevant one is debugger; (neither is setting a breakpoint using the devtools).

So setting a breakpoint in the listener, setting a debugger; statement or anything else really — would all be ignored by the browser, thus making debugging of the unload/beforeunload event kind of impossible.

The Trick To Successfully Debug unload/beforeunload Event Listeners

So back to my story… After a few hours of trying to successfully debug the suspicious unload event listener, I gave up for the night as it was very late. As I clicked the small x button in order to close the tab, expecting it to shutdown, I was amazed to finally see my breakpoint work!

Apparently there is one unloading flow in which the browser respects breakpoints and debugger; statements — not a normal reload of the page, but an actual attempt to shut its tab down completely!

I still went to sleep after that because it really was late — but ever since that I can easily debug unload/beforeunload event listeners!

<span class="token comment">// run this code in devtools console and than try to shutdown</span>
<span class="token comment">// the tab - see how Chrome suddenly respects `debugger;` statement!</span>
window<span class="token punctuation">.</span><span class="token function-variable function">onunload</span> <span class="token operator">=</span> <span class="token parameter">e</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
  <span class="token keyword">debugger</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>

 

Tips For Researchers

  1. Keep an extra tab open in your Chrome window — once you use the trick to debug listeners, although you’d be able to debug them, you will not be able to prevent the tab from shutting down. It is easier to repeatedly do so while working at the same window, which would only be possible if the closing tab is not the only tab in the window.
  2. Google Chrome Browser — I haven’t checked whether this trick works on other browsers than Chrome, mostly because I don’t see a reason to debug javascript code in any other browser (yeah, I said it).
    It might work on other browsers though — again, never even checked.
  3. Understand your case first — if the code you attempt to debug is, for some reason, session-based, closing the session (by using the trick above and closing the tab) each time might create a different flow from what you actually try to research — take that into consideration when debugging!

 

Tips For Developers

  1. No long actions inside these listeners — although practically you can synchronously do what ever you want inside the listeners, it is bad practice! Avoid long actions in order to make sure you don’t prevent the user from smooth unloading of your website.
  2. Use sendBeacon() in case you must send data before unloading — sometimes it makes sense for a website to want to send some POST data when the page is about to unload. In order to successfully do so, sendBeacon() was invented. (like, seriously, it was literally invented for this case) so make sure to use that if you want to know for sure your data is sent without expecting a response.

 

To Sum Up

Hope this made sense and helped you understand these two unique events, their purpose and their differences. Also, I hope you find that debugging trick useful — I sure did!

If I have missed out on anything or am wrong with anything of what I’ve said in this article, I would love to hear it so I can correct myself and keep this post as accurate and as helpful as possible!

Oh, and sorry for no jsfiddle demos.. for some reason beforeunload and unload events didn’t work very smoothly with it.
I recommend that you to just copy-paste the snippets above in Chrome’s devtools console in any website and see it for yourselves.

Edit: The builtin way to debug any registered event listeners in chrome devtools has come to my attention as another way to debug all beforeunload event listeners. It is indeed a great builtin way to do so, but:

  1. It can be used to debug beforeunload event only, whereas the trick I present here allows you to debug unload event as well.
  2. It only allows you to debug all registered listeners, which is very annoying and unnecessary in complex websites where there are a lot of registered event listeners and you only wish to debug a specific one.
Spread the Word