griping about a “password” system

I emailed a company today asking for my account to be linked. I did NOT ask for a password reset. What I got was an email with plain text copy of my password. Aghhhhh! That’s just asking for someone to hack my account (or all the accounts.) Passwords should be stored using a one way hash at least.

Problem 1 – username

My user id is not my last name, email or anything I have any shot of remembering. And I didn’t get to pick it. Which means it is written down.

Problem 2 – storing the password in plain text

This company shouldn’t be storing passwords in plain text or any “encoding” where they can get the original password. And the only thing I can think of to make that worse is to email the password.

Problem 3 – password requirements

Since my password was sent in the clear, I went to change it. I wanted to make it a sentence about not emailing the password. That way if someone does it again, he/she at least has to read my note. I changed the letter s to $ in my sentence as one might expect. Guess what? Only letters and numbers are allowed.

Really guys? It’s 2015.

contrast security plugin for eclipse

I recently learned that Contrast Security has a free plugin that tests your application against the OWASP Top 10.  We’ve tried to fix these already. You can read about how we fixed Clickjacking, CSRF and XSS in JForum.

Installing

I started out by installing the Contrast plugin from the Eclipse Marketplace. After restarting Eclipse, a Contrast view automatically opens with instructions. It says to right click your server and choose “Start with Contrast.” Easy enough. I usually use Sysdeo so I can start the server in one click, but this is hardly onerous.

A Diversion: Fixing Tomcat Configuration

I got an error on startup. I then tried to start the server using the server view (without Contrast) and got the same NoSuchMethodError:

java.lang.NoSuchMethodError: sun.security.ec.NamedCurve.<init>(Ljava/lang/String;Ljava/lang/String;Ljava/security/spec/EllipticCurve;Ljava/security/spec/ECPoint;Ljava/math/BigInteger;I)V

I fixed this by switching Tomcat 7 to use Java 7 instead of Java 8. (We aren’t using Java 8 yet for CodeRanch’s JForum software so this is fine.)

  • Workspace preference
  • Server
  • Runtime Environments
  • Click Tomcat and edit
  • Choose Java 7 as JRE

This had nothing to do with Contrast. I hadn’t encountered it because I was using Sysdeo to start Tomcat before this.

Actually testing

Now that the server starts up, I stopped it and restarted with Contrast. Then I clicked around the app a bit. (You can use Selenium tests or any other testing tool to automate this part.) The Contrast view starts to populate with its findings. I clicked around until I had about a dozen findings. They were:

Category Issue # Instances Details My analysis
Orange Insecure hash algorithms in XXX 3 Provides an explanation of what the problem is, why it might/might not be a problem along with the stack trace (showing how it is used) and the HTTP request/headers for the request(s) that triggered it. Two of the three findings refer to the exact same line of code. (Which was run on two different screens). The other appears to be in Tomcat itself. My configuration isn’t the same as the real server here. [The other two I need to look into further]
Yellow Anti-Caching Controls Missing in XXXX 6 Provides the HTTP request/headers, suggested remediation It’s annoying to have this reported on every page. Glad there is an :ignore this rule” option. We run a public website and want things to be cached. Client side caching makes the site faster for users and doesn’t leak information since 90% of our information is public to begin with. The only risk is if a moderator access the private forum on a public computer. We are technical users and know to clear data if this happens.
Yellow Forms without autocomplete prevention 3 Provides the HTTP request/headers, suggested remediation Again, we are a public site so not a big deal for browsers to retain information.
Warning CVE(s) in commons-httpclient-3-1.jar 1 Provides links to the two CVEs along with the manifest of the vulnerable library. I knew this from running Sonatype CLM Insight. The two CVEs are in functionality in the library that we don’t use. Still it is sweet to have this information available for free and with almost no effort. (Insight is a commercial project. We saw a one time result from the report.) I was concerned that information about the jars was being sent over the internet so I asked on Twitter. Jeff Williams replied that the CVE information is in a built in database updated via Eclipse Marketplace. Neat!

What to do with the results

When right clicking on any finding, you have four options:

  • Mark Resolved
  • Delete
  • Ignore (this instance) – useful for a false positive
  • Ignore rule – useful for a rule that doesn’t apply

My thoughts on the Contrast plugin

  • I like that the stack trace is included because it is easy to see context. I also like that lines belonging to the app is in blue in the stack trace.
  • It was very easy to use. And free. Which makes using it a no brainer.
  • While there aren’t false positives from unused code, there are false positives from context (which a tool can’t know).
  • Two of the rules triggered on a number of pages. (and would have triggered on a lot if I tested more)
  • While I don’t have a long list of things to follow up, it was a good thought exercise. And the reason I don’t have a long list is because we manually went through the OWASP top 10 in preparation for the “Iron Clad Java” promo recently. (so as not to have embarrassing issues pointed out)

fixing clickjacking and brute force login for jforum

I’ve been blogging about some of the security fixes we’ve made in the CodeRanch fork of JForum such as XSS with quotes and CSRF. Today it is time to write about Clickjacking and preventing brute force logins.

Clickjacking

Clickjacking is an attack where someone includes your site in transparent frames and the attacker intercepts anything typed in/clicked. We had originally decided not to do anything about ClickJacking because we want our site to be available in frames. For example, DZone serves the main page in a frame when asking people to vote on it.

However, that doesn’t mean that we shouldn’t protect anything. We decided to protect the most critical parts of the site – the admin screens and the login form. That leaves the read only pages (which we don’t mind framed) and editing posts/user info (which is displayed publicly anyway).

The implementation turned out to be simple. We added this header to the relevant pages. (the admin screens are always within a frame on the site and the login screen is when an admin’s session times out).

response.setHeader("X-Frame-Options", "SAMEORIGIN");

It turned out the method only needed to be called from two places: UserAction (for login) and the AdminCommand (for the admin console).

While this isn’t complete protection, it is certainly better than the nothing we had before. And it is easy to add more screens in the future if we need to. We also didn’t include the JavaScript framebusting logic for older browsers because Google Analytics says hardly of our users are on such browsers.

Preventing Brute Force Logins

Another security issue that we knew about but didn’t fix because “it wasn’t a problem” was preventing brute force logins. It seemed like high time to do something about that one too. While this was more work than fixing Clickjacking, it wasn’t a huge deal either. The logic is mostly in one (well unit tested) class that gets called from the method with the login check. There is also a property file to externalize the number of attempts/wait. And a job to clear out the Map once a day for attempts where the IP gives up and goes away.

The idea is that an IP gets a number of “free” attempts. After that more login attempts from that IP must wait longer and longer. Once a user logs in successfully from that IP or there are no attempts for a certain amount of time,t he clock starts over and the “free” attempts are back.

This approach doesn’t protect against a distributed attack because the IPs are different. (I choose the IP based approach so someone can’t lock out a user’s account specifically by targeting them and entering invalid passwords. That said, we aren’t running a bank so it seems unlikely for someone to go through that much effort to attack us.

Note: I got a private comment to be careful about IP blocking due to companies using a single public IP for thousands of users. This approach is NOT using IP blocking. Let’s say have 1000 users from InfoSys. (We probably have more, but that’s not the point.) Most of those users will either know their password or use the “remember password” functionality. If a few forget their password and type in a bunch of wrong attempts, the time throttling kicks in.  It seems unlikely for multiple users to having this problem at the same time. And as soon as anyone from the company (IP) has a correct password, the “penalty” count resets.

Code for the curious:

package net.jforum.repository;

import java.util.*;

import net.jforum.util.preferences.*;

/**
 * We prevent brute force login attacks by throttling how often we will check
 * credential after three failed logins from the same IP since the last success.
 * If there are no failures within an hour, the clock will restart on this
 * throttling.
 *
 * @author Jeanne
 * @version $Id: $
 */
public class FailedLoginRepository {
    private static final int NUM_SECONDS_IN_MINUTE = 1000 * 60;
    private static FailedLoginRepository cache = new FailedLoginRepository();
    private Map<String, List<Date>> failedLogins;
    private int maxLoginsBeforeThrottling;
    private int numMinutesForReset;
    private int[] throttlingDelay;

    // ----------------------------------------------------------------
    public FailedLoginRepository() {
        failedLogins = new HashMap<String, List<Date>>();
        numMinutesForReset = SystemGlobals.getIntValue(ConfigKeys.BRUTE_FORCE_PREVENTION_NUM_MINUTES_FOR_RESET);
        String waitTimes = SystemGlobals.getValue(ConfigKeys.BRUTE_FORCE_PREVENTION_WAIT_TIMES);
        setWaitsAsIntArray(waitTimes);
        setMaxLoginsBeforeThrottling();
    }

    private void setMaxLoginsBeforeThrottling() {
        for (int i = 0; i < throttlingDelay.length; i++) {
            if (throttlingDelay[i] == 0) {
                maxLoginsBeforeThrottling++;
            }
        }
    }

    private void setWaitsAsIntArray(String waitTimes) {
        String[] stringArray = waitTimes.split(",");
        throttlingDelay = new int[stringArray.length];
        for (int i = 0; i < throttlingDelay.length; i++) {
            throttlingDelay[i] = Integer.parseInt(stringArray[i].trim());
        }
    }

    // ----------------------------------------------------------------
    public static FailedLoginRepository getInstance() {
        return cache;
    }

    // ----------------------------------------------------------------
    /**
     * After a successful login from the API, remove the blocks. This indicates
     * the user just mistyped and not a hacker.
     *
     * @param ip
     */
    public void successfulAttempt(String ip) {
        failedLogins.remove(ip);
    }

    // ----------------------------------------------------------------
    /**
     * Keep track of time of login failure
     *
     * @param ip
     * @param now
     */
    public void failedAttempt(String ip, Date now) {
        removeOldAttempts(ip);
        List<Date> fails = failedLogins.get(ip);
        if (fails == null) {
            fails = new ArrayList<Date>();
            failedLogins.put(ip, fails);
        }
        fails.add(now);
    }

    public void failedAttempt(String ip) {
        failedAttempt(ip, new Date());
    }

    // ----------------------------------------------------------------
    /**
     * If there haven't been any failed attempts from that IP in the last hour,
     * start over.
     *
     * @param ip
     */
    private void removeOldAttempts(String ip) {
        List<Date> fails = failedLogins.get(ip);
        if (fails != null) {
            Date lastFail = getLastFail(fails);
            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.MINUTE, -numMinutesForReset);
            if (lastFail.before(cal.getTime())) {
                failedLogins.remove(ip);
            }
        }
    }

    private Date getLastFail(List<Date> fails) {
        Date lastFail = fails.get(fails.size() - 1);
        return lastFail;
    }

    public void removeAllOldAttempts() {
        for (String ip : failedLogins.keySet()) {
            removeOldAttempts(ip);
        }
    }

    // ----------------------------------------------------------------
    public boolean shouldCheckPassword(String ip) {
        removeOldAttempts(ip);
        int waitRemaining = numberMinutesUntilNextAllowedLogin(ip, new Date());
        return waitRemaining == 0;
    }

    public boolean shouldCheckPassword(String ip, Date now) {
        removeOldAttempts(ip);
        List<Date> fails = failedLogins.get(ip);
        return fails == null || fails.size() <= maxLoginsBeforeThrottling;
    }

    // ----------------------------------------------------------------
    public int numberMinutesUntilNextAllowedLogin(String ip) {
        return numberMinutesUntilNextAllowedLogin(ip, new Date());
    }

    /**
     * Contains algorithm for delays: see if already waited number of minutes
     * need to wai
     *
     * @param ip
     * @return
     */
    public int numberMinutesUntilNextAllowedLogin(String ip, Date now) {
        List<Date> fails = failedLogins.get(ip);
        if (fails == null) {
            return 0;
        }
        int index = fails.size() - 1;
        if (index >= throttlingDelay.length) {
            index = throttlingDelay.length - 1;
        }
        int delay = throttlingDelay[index];
        Date lastFailure = getLastFail(fails);
        int numMinutesWaited = (int) (now.getTime() - lastFailure.getTime()) / NUM_SECONDS_IN_MINUTE;
        int result = delay - numMinutesWaited;
        // if delay is over, do need to wait longer
        if (result < 0) {
            result = 0;
        }
        return result;
    }
}