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