Рубрики
Без рубрики

Ключи карты Java всегда должны быть сопоставимы

Если вы реализуете функции equals() и hashCode(), вам также следует реализовать функцию compareTo(). Помеченный как java, программирование, безопасность.

Эта статья вдохновлена открытием много лет назад, что злоумышленник может манипулировать параметрами запроса, чтобы превратить структуру данных карты на многих веб-серверах в связанный список, заставляя все имена параметров попадать в один и тот же хэш-блок, о котором мне напомнил CVE-2018-0875. Вы можете прочитать об оригинальном открытии в сертификате VU#903934 и оригинальное сообщение в архивах списка полного раскрытия или на YouTube от 28c3 .

В Java, например, строки "fg" , "gH" и "h)" все имеют хэш-код 3265, поэтому злоумышленник может создавать строки запроса, такие как fg fg=0&fg=0&fg)=0&gHfg=0

Сама Java реализовала защиту в классе String в начале выпусков Java 7, описанную в Улучшениях платформы коллекций в Java 7 . Класс String получил новый метод и поле hash32 , в котором использовался MurmurHash3 со случайным начальным значением. Это хорошо для строк и HashMap s, но не так хорошо для пользовательских ключей.

Лучший подход был реализован в JEP 180 для Java 8. Теперь, если слишком много хэш-кодов сопоставляется с одним и тем же сегментом карты, список записей можно преобразовать в сбалансированное двоичное дерево, отсортированное сначала по хэш-коду, а затем по методу compareTo каждого ключа, если ключи Сопоставимы . Очевидно, что это относится к строкам (и имеет особый случай в HashMap ), но это также должно относиться к каждому классу, используемому в качестве ключа карты, особенно если данные потенциально предоставляются враждебным пользователем.

Внедрение Сопоставимых

Это просто реализовать равно() и Хэш-код() правильно для класса данных, если немного подробен:

public final class SearchKey {
    private final String site;
    private final long userId;
    private final String query;

    public SearchKey(String site, long userId, String query) {
        this.site = Objects.requireNonNull(site);
        this.userId = userId;
        this.query = Objects.requireNonNull(query);
    }

    // getters ...

    @Override
    public int hashCode() {
        return (site.hashCode() * 31 + Long.hashCode(userId)) * 31 + query.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof SearchKey)) {
            return false;
        }
        SearchKey other = (SearchKey) obj;
        return userId == other.userId && site.equals(other.site) && query.equals(other.query);
    }
}

(Я использую 31 в качестве множителя в хэш-коде, потому что он простой, и x * 31 может быть оптимизирован до (x << 5) - x чтобы быть намного быстрее на некоторых платформах.)

Как указано выше, один и тот же хэш-код может быть сгенерирован для разных значений, например новый ключ поиска("fg", 12345, "fg") , новый ключ поиска("fg", 12345, "GH") и новый поисковый ключ ("GH", 12345, "fg") , все хэши которого равны 3,523,625. Если бы мы использовали их в качестве ключей в кэше на основе карт, злоумышленник мог бы организовать атаку типа “отказ в обслуживании”, выполнив множество запросов к нашей службе, которые используют один и тот же хэш.

Метод compareTo может быть реализован путем утомительного сравнения полей одно за другим:

public final class SearchKey implements Comparable {
    // ...

    @Override
    public int compareTo(SearchKey o) {
        int c = site.compareTo(o.site);
        if (c != 0) {
            return c;
        }
        c = Long.compare(userId, o.userId);
        if (c != 0) {
            return c;
        }
        return query.compareTo(o.query);
    }
}

Но с Java 8 это может быть реализовано более лаконично:

public final class SearchKey implements Comparable {
    private static final Comparator COMPARATOR =
        Comparator.comparing(SearchKey::getSite)
            .thenComparingLong(SearchKey::getUserId)
            .thenComparing(SearchKey::getQuery);

    // ...

    @Override
    public int compareTo(SearchKey o) {
        return COMPARATOR.compare(this, o);
    }

Оригинал: “https://dev.to/carey/java-map-keys-should-always-be-comparable-2c1b”