The Missing Bit

Don't leave password around in SPA
June 10, 2017
Updated: February 22, 2018

security javascript spa

There is one thing I have noticed with SPA (single page app) login forms. Many of them keep the password in memory. And when I say in memory, I don’t mean as freed() memory which may require freezing your PC’s RAM to access, I mean as actual living object within the javascript state.

For example, given the following pseudo code:

state = {
    login = {
        username = ""
        password = ""
    currentPage = "login"

onFormInput =>
    state.login.username = usernameInput.value
    state.login.password = passwordInput.value

onFormSubmit =>
    loginRequest state.login then =>
        state.currentPage = "home"

After this kind of code, on successful login, the user credential stays in memory at state.login. The credentials will also stay in memory if the user is idle.

An attack vector would be: the user login to your blogging SPA, once logged in, the credentials stay in memory as shown above. Your blogging SPA has a plugin API, where people can write plugins with arbitrary code. You didn’t sandbox the plugin architecture correctly, and the plugin code has access to your state. This gives the attacker direct access to the unencrypted password and username.

Also, some library might keep the input field somewhere in memory while it is removed from the DOM. The browser might have a bug…

The mitigation is quite simple, while the form is submitted, replace the password in the input by a string of the same length, this way, when the authentication is in progress, the password field is not empty and the number of dot didn’t change. After successful login, wipe the state.

The other good measure is to empty the login form after a minute of inactivity.

In pseudo code:

onFormInput =>
    state.login.username = usernameInput.value
    state.login.password = passwordInput.value

    clearTimeout resetPassword

    resetPassword = setTimeout 60, =>
        state.login.password = ""

onFormSubmit =>
   local temporaryCredentials = state.login
   state.login = {
       username = temporaryCredentials.username
       password = "." * length temporaryCredentials.password
   loginRequest temporaryCredentials then =>
        state.currentPage = "home"
        state.login = { username = "", password = "" }
   # temporaryCredentials should get out of scope here
   # also make sure you are not holding to requests objects
   # containing the credentials

Keeping the username in the state may be OK, as I’m quite sure it is somewhere else in your state, it depends on how sensible your app is (with something like plugins, you may consider keeping only some kind of display name around and not the email).

The other thing to consider is the need for to have your login form as a part of your SPA. I personally prefer having the login form on its own “JS free” page, even for SPA. You might lose some goodies (like transition between successful login and your app UI) but I think the security benefit is more important.

Also consider supporting external authentication providers (Facebook, Google, Twitter, github if your audience is technical…).

If you wish to comment or discuss this post, just mention me on Mastodon or Twitter.

Related posts