With Easy Passwords I develop a product which could be considered a Last Pass competitor. In this particular case however, my interest was sparked by the reports of two Last Pass security vulnerabilities (1, 2) which were published recently. It’s a fascinating case study given that Last Pass is considered security software and as such should be hardened against attacks.
I decided to dig into Last Pass 4.1.21 (latest version for Firefox at that point) in order to see what their developer team did wrong. The reported issues sounded like there might be structural problems behind them. The first surprise was the way Last Pass is made available to users however: on Addons.Mozilla.Org you only get the outdated Last Pass 3 as the stable version, the current Last Pass 4 is offered on the development channel and Last Pass actively encourages users to switch to the development channel.
My starting point were already reported vulnerabilities and the approach that Last Pass developers took in order to address those. In the process I discovered two similar vulnerabilities and a third one which had even more disastrous consequences. All issues have been reported to Last Pass and resolved as of Last Pass 4.1.26.
Last Pass on the other hand supports filling in passwords without any user interaction whatsoever, even though that feature doesn’t seem to be enabled by default. But that’s not even the main issue, as Mathias Karlsson realized the code recognizing which website you are on is deeply flawed. So you don’t need to control a website to steal passwords for it, you can make Last Pass think that your website
malicious.com is actually
twitter.com and then fill in your Twitter password. This is possible because Last Pass uses a huge regular expression to parse URLs and this part of it is particularly problematic:
This is meant to match the username/password part before the hostname, but it will actually skip anything until a
@ character in the URL. So if that
@ character is in the path part of the URL then the regular expression will happily consider the real hostname part of the username and interpret anything following the
@ character the hostname — oops. Luckily, Last Pass already recognized that issue even before Karlsson’s findings. Their solution? Add one more regular expression and replace all
@ characters following the hostname by
%40. Why not change the regular expressions so that it won’t match slashes? Beats me.
The bug that Karlsson found was then this band-aid code only replacing the last
@ character but not any previous ones (greedy regular expression). As a response, Last Pass added more hack-foo to ensure that other
@ characters are replaced as well, not by fixing the bug (using a non-greedy regular expression) but by making the code run multiple times. My bug report then pointed out that this code still wasn’t working correctly for
data: URLs or URLs like
http://email@example.com:firstname.lastname@example.org/. While it’s not obvious whether the issues are still exploitable, this piece of code is just too important to have such bugs.
Of course, improving regular expressions isn’t really the solution here. Last Pass just shouldn’t do their own thing when parsing URLs, it should instead let the browser do it. This would completely eliminate the potential for Last Pass and the browser disagreeing on the hostname of the current page. Modern browsers offer the URL object for that, old ones still allow achieving the same effect by creating a link element. And guess what? In their fix Last Pass is finally doing the right thing. But rather than just sticking with the result returned by the
URL object they compare it to the output of their regular expression. Guess they are really attached to that one…
I didn’t know the details of the other report when I looked at the source code, I only knew that it somehow managed to interfere with extension’s internal communication. But how is that even possible? All browsers provide secure APIs that allow different extension parts to communicate with each other, without any websites listening in or interfering. To my surprise, Last Pass doesn’t limit itself to these communication channels and relies on window.postMessage() quite heavily in addition. The trouble with this API is: anybody could be sending messages, so receivers should always verify the origin of the message. As Tavis Ormandy discovered, this is exactly what Last Pass failed to do.
In the code that I saw origin checks have been already added to most message receivers. However, I discovered another communication mechanisms: any website could add a form with
id="lpwebsiteeventform" attribute. Submitting this form triggered special actions in Last Pass and could even produce a response, e.g. the
getversion action would retrieve details about the Last Pass version. There are also plenty of actions which sound less harmless, such as those related to setting up and verifying multifactor authentication.
For my proof-of-concept I went with actions that were easier to call however. There was
get_browser_history_tlds action for example which would retrieve a list of websites from your browsing history. And there were
getuuid actions which allowed saving an identifier in the Last Pass preferences which could not be removed by regular means (unlike cookies).
Last Pass resolved this issue by restricting this communication channel to lastpass.com and lastpass.eu domains. So now these are the only websites that can read out your browsing history. What they need it for? Beats me.
When looking into other interactions with websites, I noticed this piece of code (reduced to the relevant parts):
var src = window.frameElement.getAttribute("lpsrc"); if (src && 0 < src.indexOf("lpblankiframeoverlay.local")) window.location.href = g_url_prefix + "overlay.html" + src.substring(src.indexOf("?"));
This is how Last Pass injects its user interface into websites on Firefox: since content scripts don’t have the necessary privileges to load extension pages into frames, they create a frame with an attribute like
lpsrc="http://lpblankiframeoverlay.local/?parameters". Later, the code above (which has the necessary privileges) looks at the frame and loads the extension page with the correct parameters.
Of course, a website can create this frame as well. And it can use a value for
lpsrc that doesn’t contain question marks, which will make the code above add the entire attribute value to the URL. This allows the website to load any Last Pass page, not just
overlay.html. Doesn’t seem to be a big deal but there is a reason why websites aren’t allowed to load extension pages: these pages often won’t expect this situation and might do something stupid.
tabDialog.html indeed does something stupid. This page still has a message handler reacting to messages sent via
The security issues discovered in Last Pass are not an isolated incident. The base concept of the extension seems sound, for example the approach they use to derive the encryption key and to encrypt your data before sending it to the server is secure as far as I can tell. The weak point is the Last Pass browser extension however which is necessarily dealing with decrypted data. This extension is currently violating best practices which opens up unnecessary attack surfaces, the reported security vulnerabilities are a consequence of that. Then again, if Tavis Ormandy is right then Last Pass is in good company.