Эта статья вдохновлена открытием много лет назад, что злоумышленник может манипулировать параметрами запроса, чтобы превратить структуру данных карты на многих веб-серверах в связанный список, заставляя все имена параметров попадать в один и тот же хэш-блок, о котором мне напомнил 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”