Facebook Likejacking Prevention
is the second plug-in that I've released for multiple browsers (Google Chrome, Safari and Firefox). It is technically much more complex than the previous one, Zscaler Safe Shopping
Each browser offered different challenges. As promised in the previous blog post
, I will provide details of what was required to port the plugin to different platforms.
Facebook Likejacking Prevention 1.0.8
Before I start on the technical details, I'd like to remind all users to upgrade to the latest version
of the add-on (currently 1.0.8). In Firefox and Chrome, go to Add-ons or Extensions, and check for updates. For Safari users, please install the latest version from our website
. I have fixed a couple of bugs, and made several improvements in the detection of suspicious pages.
This is a fairly long and technical post. Don't hesitate to post a comment if some points are not clear.
There are two main components in the plugin. The first feature involves finding the Facebook widgets inside a page. These widgets are iframes hosted on www.facebook.com
. Then the plugin needs to inspect the containers, the HTML elements that contain the IFRAME, to figure out if the widget is hidden or not. The protection (widget removal or explicit confirmation) needs to be applied on each widget, as chosen by the user.
The second element involves displaying relevant information to the user: a quick view indicating whether the page is suspicious or not and a more detailed view for additional information.
Extensions on Google Chrome are comprised of two parts:
- A background page which has access to the browser: local storage, extension options, tab events, etc., but it does not have access to the content of the tabs, i.e the HTML content
- A content script injected in each page/frame/iframe, which has access to the HTML content (read and write), but does not have access to the browser or other tabs. The content script injected in one frame does not have access to other frames or iframes, even within the same tab.
The background page and the content scripts can communicate with each other by sending messages.
In Zscaler Likejacking Prevention, the content scripts are charged with finding the Facebook widgets, inspecting the containers, and sending the information back to the background page. The background page aggregates the information for each tab, and decides which action to take on the tab content depending upon the options and whether or not suspicious elements where found.
This architecture works well for the add-on, except in these two scenarios:
Layers of Frames/Iframes
Facebook widgets can be loaded inside an iframe or a frame, which can itself be loaded inside an iframe, etc. In order to inspect the container, the content script would have to cross the boundaries of the frame/iframe it was injected in. But this is not allowed by the browser. So doing the DOM inspection for the container from bottom (the Facebook widget) to top (the main document) by each injected script is not possible.
Instead, container inspection has to be done with a mix of bottom up and top down. The content script in the main document does have read access to all the frames/iframes which it resides in. After all Facebook iframes are found, the container inspection is done from the widget to its containing iframe (bottom up).
The parent container is found with the parentNode
of an iframe is shown as null
. In order to find the container of an iframe, it has to be found from the top document down to the iframe based on it's SRC attribute. Finally, a new score is computed by the main document and sent back to the background page. If an action needs to be taken, the background page sends a message to all injected scripts inside the tabs.
The page is inspected as soon as it is loaded, in order to apply the protection quickly. However, Facebook widgets can be inserted at any time, including after a page has finished loading. So the page score must be recalculated every time a new iframe is loaded.
To solve the problem, when the background page gets a message from an injected script inside a Facebook iframe, it checks if any Facebook widgets were already found in this tab. If not, the background page asks for a new evaluation of the page.
Icon and Popup
Chrome has the best architecture for showing information for a specific page. It was very easy to show an icon associated with a popup for a given tab. No problem there.
Safari (Opera and Firefox Mobile as well) has the same plug-in architecture as Google Chrome, with the separation between the background page (referred as global page
by Safari) and content script. Therefore, it did offer the same challenges as Chrome.
However, some technical limitations in Safari did bring a few more issues...
One way communication
While each injected script can send a message to the global page, the global page can only send a message to the main page, not to each frame/iframe. To be more specific, the global page can reply back to a message from a frame/iframe, but cannot initiate the message to them. That means once it has aggregated the information from all content scripts, it can push a message to apply the protection to the main page only. To get around this limitation, each frame/iframe has to send a message to the global page regularly to check if there is any action to be taken.
Storing the aggregate information
In Chrome, each tab has a unique ID. The aggregate information is associated with the tab ID. There is no such ID in Safari. Instead, the information can be saved as a property of the tab. Unfortunately, if the tab is moved to a new window by the user, the associated property is reset, and the information is lost. I have to save the information as a tab property (the most accurate way), but also based on the URL in case the tab is moved. If there are different tabs with the same URL, but different content, the data can be mixed up.
I had to find a creative way to get around the UI limitations of Safari. First of all, it is not possible to have an icon in the URL bar as I do in Chrome and Firefox. Then, toolbar buttons are black and white only, and can have only 2 states. I need 3 states for the button: no Facebook widgets on the page, safe page with Facebook widgets and suspicious page.
Instead of an icon or button, I had to use a toolbar. I also had to take care when it came to hiding and showing the toolbar when the user switched tabs, loaded a new URL, etc.
I actually had to use two toolbars, because each is limited to a fixed height of 30 pixels. One toolbar is not enough for all the information and links I want to show. The detailed information and actions have to be shown on a second toolbar!
However, it was harder to detect new frames/iframes being loaded or added by just listening to page load events. The solution I use is to listen to HTTP requests for HTML documents and re-inspect the tab every time a new element was sent.
The UI was a bit more tricky than in Google Chrome. There are icons associated with a popup called Page action. Unfortunately, the popup content is limited to a piece of text and one button. This does not work with the information and actions I needed to offer.
Instead, I had to create my own "Page Action" by adding and removing an icon in the URL bar as needed and showing a toolbar at the top of the tab when the user clicks on the icon for more information. Overall, it required a bit more work than Google Chrome, but was much easier than for Safari
It was interesting to see the impact of the different browser architectures and limitations on my plugin. Safari was by far the hardest platform to work with. DOM inspection and manipulation was the easiest in Firefox, while displaying information in the UI was best done in Google Chrome.
It is a bit disappointing that it was not possible to have the plugin look the same in the different browsers. Google has the layout I really wanted and I found the UI restrictions in Safari very problematic.