#vaadin

JAVAPROjavapro
2025-06-08

Wie entsteht aus der harmlosen ein Sicherheitsrisiko? Ganz einfach: mutable Schlüssel + veränderte Attribute = unsichtbare Einträge. Perfekter Nährboden für Angriffe!

@svenruppert zeigt Probleme & Strategien zur Vermeidung: javapro.io/de/wenn-hashcode-lu

Sven Ruppertsvenruppert
2025-06-08

Java-Dev? Nutzt du veränderliche Objekte als HashMap-Key?

Dann droht: Datenverlust, Cache-Versagen, Exploits durch Race-Conditions.

Habe ein Demo gebaut, das genau diese Schwachstelle zeigt – mit Core Java, und Flow

javapro.io/de/wenn-hashcode-lu

If hashCode() lies and equals() is helpless

A deep look into Java’s HashMap traps – visually demonstrated with Vaadin Flow.

The silent danger in the standard library

The use of HashMap and HashSet is a common practice in everyday Java development. These data structures offer excellent performance for lookup and insert operations, as long as their fundamental assumptions are met. One of them is hashCode() of a key remains stable. But what if that’s not the case?

This is precisely where one of the most subtle and dangerous traps of the Java standard library lurks: mutable key objects. In this article, we not only demonstrate why this constellation is problematic but also illustrate the phenomenon interactively using Vaadin Flow. Readers will learn how the HashMap works internally, why equals() alone is not enough, and how to use modern language tools to generate robust, immutable keys.

  1. The silent danger in the standard library
  2. The fundamental problem: identity, hash codes and lookup
  3. The classic mistake hashCode() depends on variable attributes
  4. Security-critical consequences: When loss of consistency becomes an attack surface
  5. Interactive Demo with Vaadin Flow
  6. HashSet
  7. Strategies to avoid
  8. Why are other map implementations not affected
  9. Conclusion: The price of convenience
  10. Demo source code to try it out yourself

The fundamental problem: identity, hash codes and lookup

Internally, each HashMap is an array of buckets, where the hash code of the key determines the position of an entry. The insertion process (put(K key, V value)) looks like this: First, the map calls key.hashCode() and calculates the index in the bucket array from this value, using internal spreading. If entries already exist at this position (e.g., due to hash collisions), a linear search is performed within the bucket or – if there are enough collisions – i.e., if there are more than eight entries in a bucket and the underlying array exceeds a specific size (default value: 64) – the bucket is internally transformed from a simple linked list into a balanced binary tree structure (more precisely: a red-black tree). This conversion improves the lookup performance from linear time O(n) to logarithmic time O(log n). The decision for these thresholds is based on the observation that collisions are rare in well-chosen hash functions, and the overhead of a tree structure is only worthwhile with a high entry density. equals() is used to identify the appropriate key or create a new entry.

When accessing (get(Object key)) the same process occurs: The hash code of the transferred key is calculated again, the corresponding bucket is determined and searched for a matching key via equals() However, if the hash code of the object differs from the original put() have changed, a different bucket is addressed – the original entry then remains invisible.

Removing (remove(Object key)) is subject to the same mechanisms: the map searches for the correct bucket via hashCode() and then compares the keys using equals(). Consistency of the hash-relevant data over the entire lifetime of the key is therefore essential. Only if this is guaranteed can the HashMap ensure its efficiency and correctness.

This means: Even access to the correct bucket depends exclusively on the result of the method hashCode() This value is calculated immediately upon access, combined with an internal transformation (such as bitwise shifting and XOR operations to achieve a more even distribution across the bucket array), and then used to index the bucket array. If the return value of hashCode(). After inserting an object into the map, for example, by mutating an attribute that is included in the calculation, the newly calculated index points to a different bucket. However, the object being searched for does not exist there, which is why the map can no longer find the entry. This behaviour is not a malfunction of the HashMap, but the direct consequence of a breach of the fundamental contract hashCode() must remain consistent while maintaining the same internal object state. Violating this contract is abusing the HashMap in a way it was not designed for, with potentially serious consequences for data integrity and program logic.

The classic mistake hashCode() depends on variable attributes

Let’s take the following example: An instance of the class Person with the values name = “Alice” and id = 42 is created and stored as a key in a HashMap At the time of insertion, the map calculates the hashCode() based on the current state of the object – i.e. Objects.hash(“Alice”, 42) – and saves the entry in the corresponding bucket. After that, the field name of the object, e.g., is set to the value “Bob”. This changes the return value of hashCode(), for example, Objects.hash(“Bob”, 42), which addresses a different bucket.

If you now try to access the map again with the same object, map.get(originalPerson) – the operation fails. The map calculates a new index from the current hash code, looks in the corresponding bucket, and doesn’t find a matching entry. The original object is technically still in the map, but cannot be found.

The situation becomes even more misleading when one considers the entrySet() method. The entry is visible there, since the iteration is carried out directly via the internal chains, independent of the hash code. A comparison via equals() works with a newly created, identical object – after all, equals() is typically based on content equality and not on memory address or hash code.

This makes the HashMap de facto inconsistent: The element is still physically stored in the internal data array, but cannot be accessed via regular access paths, such as get(key), can no longer be found. To demonstrate this effect reproducibly, consider the following code example:

import java.util.*;class Person {    String name;    you hand;    Person(String name, int id) {        this.name = name;        this.id = id;    }    @Override    public boolean equals(Object o) {        return o instanceof Person p && Objects.equals(name, p.name) && id == p.id;    }    @Override    public int hashCode() {        return Objects.hash(name, id);    }}public class MutableHashDemo {    public static void main(String[] args) {        Person p = new Person("Alice", 42);        Map<Person, String> map = new HashMap<>();map.put(p, "Value");System.out.println("Before change:");        System.out.println("map.get(p): " + map.get(p));// Mutation of the key        p.name = "Bob";System.out.println("After change:");        System.out.println("map.get(p): " + map.get(p));        System.out.println("Enthält key via entrySet: " + map.entrySet().stream()            .anyMatch(e -> e.getKey().equals(p)));    }}

The demo source code is available on github at https://github.com/svenruppert/Blog—Core-Java—Mutable-HashMap-Keys-in-Java/blob/main/src/test/java/junit/com/svenruppert/MutableHashCodeDemoTest.java 

This example demonstrates that map.get(p) returns null after the change, although the entrySet() still contains the entry. The reason hashCode() returns a different value after the mutation so that the original bucket is no longer addressed. equals() alone does not help in this case, since the lookup fails due to the wrong index.

Security-critical consequences: When loss of consistency becomes an attack surface

While the loss of referentiality in a HashMap may appear at first glance to be merely a technical issue, closer inspection reveals security-relevant implications, particularly in systems that utilise state caching, authentication, or access control. If an object serves as a key in security-critical maps or sets – e.g., for detecting active sessions, auth tokens, or user permissions – and its hash code subsequently changes, a logical error condition arises. The system no longer “sees” the object, even though it still exists. This can lead to unintended access gaps or, worse still, unattended persistence of state.

A particularly dangerous scenario arises when mutable keys can be influenced from outside. One conceivable scenario is where an attacker can inject controlled values ​​into an object that then serves as a key. As soon as this key is changed—for example, through manipulated API usage or faulty deserialisation— it is removed from access control, even though it technically still exists. The result: logical access without valid authorisation.

In extreme cases, this can even lead to typical security-critical patterns, such as:

  • Authorisation Bypass: An object is modified before the test, the contains() fails, and access is granted incorrectly.
  • Resource Lock Hijack: A lock object is created via a HashSet or Map managed by the system, but it can no longer be removed after mutationa deadlock or race condition threatens.
  • Denial of service due to hash collisions: If a system stores (or mutates) many mutable objects with controlled hash codes, hash collisions can be deliberately triggered, and performance problems can be created.

Although a classic buffer overflow doesn’t occur directly in Java due to the memory safety of the JVM model, the structural effect of an inconsistent HashMap is similar to an overflow at the logical level: An access lands in the “wrong memory area” (bucket), and the object is present but functionally invisible. This is a key point for security auditors and architects: Loss of consistency in hash-based structures can not only lead to incorrect behaviour but also become a potential entry point for complex attacks.

Interactive Demo with Vaadin Flow

To make the described problem tangible, a minimalist demonstration application is recommended, e.g., with Vaadin Flow. The demo aims to allow the user to observe live how an object in a HashMap becomes “invisible” after a mutation.

The application consists of a simple Vaadin view with the following UI elements:

  • Input fields for name and ID
  • A button to insert an object into a HashMap
  • A button to modify the name (and thus the hash code)
  • A button to execute map.get()
  • A button for iterating over entrySet()

The view constructor first creates the graphical user interface. Two input fields – one for the name, one for the ID – are used to interact with the Person-Object. 

private final TextField nameField = new TextField("Name");private final TextField idField = new TextField("ID");private final TextArea output = new TextArea("Output");private final Person mutableKey = new Person("Alice", 42);private final Map<Person, String> map = new HashMap<>();

Several buttons allow you to perform specific operations on the Map. The “put(key, value)” button adds the current mutable Key-Object as key with the value “Saved” to the map. This uses exactly the object whose name and id were determined at the beginning – here “Alice” and 42.

Button putButton = new Button("put(key, value)", _ -> { map.put(mutableKey, "Saved");});

About the button “Change name”, the name of the mutableKey object at runtime. This leads to a state in which the contents of the object—and thus its hash code—change after it is inserted into the map.

Button mutateButton = new Button("Change name", _ -> { mutableKey.name = nameField.getValue();});

The “get(key)” Button demonstrates this effect: The method map.get(mutableKey) attempts to retrieve the value using the (modified) object as a key.

Button getButton = new Button("get(key)", and -> { String result = map.get(mutableKey); output.setValue("Result of get(): " + result);});

To illustrate this effect and, at the same time, provide an alternative access, the button “search entrySet()” has been added. It manually iterates through all key-value pairs of the map using the Stream API and compares the keys via equals(), regardless of the internal bucket structure. This means that an entry with the changed key object can still be found, provided equals() is correctly implemented and independent of the hashCode function.

Button iterateButton = new Button("search entrySet()", and -> { String result = map.entrySet().stream()     .filter(entry -> entry.getKey().equals(mutableKey))     .map(Map.Entry::getValue)     .findFirst()     .orElse("Not found via equals()"); output.setValue("entrySet(): " + result);});//Here is the complete source code of the view.@Route(value = PATH, layout = MainLayout.class)public class VersionOneView   extends VerticalLayout   implements HasLogger { public static final String PATH = "versionone"; private final TextField nameField = new TextField("Name"); private final TextField idField = new TextField("ID"); private final TextArea output = new TextArea("Output"); private final Person mutableKey = new Person("Alice", 42); private final Map<Person, String> map = new HashMap<>(); public VersionOneView() { logger().info("Initializing VersionOneView"); nameField.setValue(mutableKey.getName()); idField.setValue(String.valueOf(mutableKey.getId())); output.setWidth("600px"); Button putButton = new Button("put(key, value)", _ -> {   logger().info("Putting value into map with key: {}", mutableKey);   map.put(mutableKey, "Saved");   output.setValue("Inserted: "+ mutableKey); }); Button mutateButton = new Button("Change name", _ -> {   logger().info("Changing name from {} to {}", mutableKey.getName(), nameField.getValue());   mutableKey.setName(nameField.getValue());   output.setValue("Name changed to: "+ mutableKey.getName()); }); Button getButton = new Button("get(key)", and -> {   logger().info("Getting value for key: {}", mutableKey);   String result = map.get(mutableKey);   logger().info("Get result: {}", result);   output.setValue("Result of get(): " + result); }); Button iterateButton = new Button("search entrySet()", and -> {   logger().info("Searching through entrySet for key: {}", mutableKey);   String result = map.entrySet().stream()       .filter(entry -> entry.getKey().equals(mutableKey))       .map(Map.Entry::getValue)       .findFirst()       .orElse("Not found via equals()");   logger().info("EntrySet search result: {}", result);   output.setValue("entrySet(): " + result); }); add(nameField, idField, putButton, mutateButton, getButton, iterateButton, output); logger().info("VersionOneView initialization completed");} public class Person {   String name;   int id;   public Person(String name, int id) {     this.name = name;     this.id = id;   }//SNIP getter setter   @Override   public boolean equals(Object o) {     return theinstanceof Person p             && Objects.equals(name, p.name)             && id == p.id;   }   @Override   public int hashCode() {     return Objects.hash(name, id);   }   @Override   public String toString() {     return name + " (" + id + ")";   } }

This concise yet powerful view enables any reader to directly observe the effects of changing keys.

The demo source code is available on github athttps://github.com/svenruppert/Blog—Core-Java—Mutable-HashMap-Keys-in-Java/blob/main/src/main/java/com/svenruppert/flow/views/version01/VersionOneView.java  

Now, let’s modify the view slightly and display all entries from the EntrySet. The hash codes are compared, and the result is output.

Button iterateButton = new Button("search entrySet()", _ -> { logger().info("Searching through entrySet for key: {}", mutableKey); StringBuilder result = new StringBuilder(); map.forEach((key, value) -> {   boolean isMatch = key.equals(mutableKey);   result.append(String.format("""                                   Key: %s, Value: %s, HashCode %s Match: %s""",                               key,                               value,                               key.hashCode(),                               isMatch ? "And" : "No"));   result.append("\n"); }); logger().info("EntrySet search result: {}", result); output.setValue("entrySet():\n" + (!result.isEmpty() ? result.toString() : "Map is empty"));});

Now you can clearly see how an instance now exists multiple times in the same hash map. If you think about it, you can think of some very unpleasant applications that could even be used to specifically attack a system. But more on that in another blog post.

HashSet

Since a HashSet internally is nothing other than a HashMap, where the keys are the actual set elements and the values ​​are a constant dummy (such as PRESENT = new Object()), all the problems described here apply in the same way. If an object is modified after being inserted into the set, and this change affects one of the attributes that is used in the calculation of hashCode(), the object is searched in the wrong bucket. The method contains() returns false, even though the object is stored in the set. remove() fails because the same logic as get() takes effect: the HashMap finds the bucket using the current hash code and does not recognise the key there.

This behaviour is particularly critical when HashSet is used to temporarily store security-relevant information, for example, to manage already authenticated users, valid tokens, or temporary permissions. An unintentionally mutated key can lead to access being incorrectly denied or even security checks being bypassed—a classic case of a logical security vulnerability that is barely noticeable and difficult to debug.

In safety-critical modules, set elements should therefore always be immutable and defensively constructed. This means, in concrete terms, no changeable fields, no public modifiability, and – ideally – construction via a record or builder with final attributes.

Set<Person> people = new HashSet<>();people.add(p); // p.hashCode() ist Xp.name = "Malicious";System.out.println(people.contains(p)); // false

Strategies to avoid

The simplest and most effective strategy is to use only immutable objects as keys. Since Java 16, records are the idiomatically correct tool for this. They automatically generate equals() and hashCode() based on final fields.

record PersonKey(String name, int id) {}

Alternatively, if mutable state is unavoidable, the object before A modification can remove the hash code from the map and then reinsert it with an updated hash code. This is a dangerous workaround that is rarely implemented correctly in practice.

Why are other map implementations not affected

The problem described here occurs specifically in HashMap and structures based on it, such as HashSet, because a calculated hash code determines the access path. Other map implementations in the JDK – such as TreeMap, LinkedHashMap, or EnumMap – are not affected in this respect or only to a lesser extent.

The TreeMap, for example, is not based on hash code-based indexing, but on a sorted tree structure (red-black tree). It uses either the natural order of the keys (via Comparable) or a provided Comparator. This means that access does not depend on the result of hashCode() but from the stable comparability through compareTo() or compare(K1, K2). A change to an attribute that is included in the comparison logic can also lead to inconsistent behaviour, for example, get() or remove(). However, the mechanism is more transparent and controllable because sorting is done using a clearly defined comparison function.

Also, LinkedHashMap, although internally a HashMap, is not immune to the mutable hashcode problem because it uses the same bucket logic. However, it also provides a deterministic iteration order, which can aid in debugging, but does not alter the underlying problem.

The EnumMap. Finally, it is protected against this problem by design, as it is exclusively of enum types as keys. These are immutable by language definition and have stable, final hash codes. Thus, mutation is impossible, and the map remains robust against this class of errors.

You should therefore carefully consider which map implementation is used in each context, and whether a stable key state can be guaranteed or whether alternative ordering mechanisms should be used.

Conclusion: The price of convenience

In daily development, the problem is often overlooked because it only manifests itself in specific situations – for example, after multiple operations, in multi-threading, or integrations across API boundaries. However, the consequences are severe: lost entries, unexplained null values, and inconsistent state.

Anyone who wants to use HashMaps efficiently and correctly should respect their internal rules, particularly ensuring that key objects do not change subsequently.

Demo source code to try it out yourself

The complete source code for the demo view with Vaadin Flow is publicly available on GitHub. The application can be run efficiently locally using’ mvn jetty: run’. The web application will then be available at the address http://localhost:8080/.

GitHub: https://github.com/svenruppert/Blog—Core-Java—Mutable-HashMap-Keys-in-Java

Happy Coding

Sven

#Flow #Java #secureCodingPractices #Vaadin

JAVAPROjavapro
2025-06-03

Datei-Uploads in sicher machen?

Schütze dich vor:
- CWE-22 (Path Traversal)
- CWE-377 (Temp File Risks)
- CWE-778 (Insufficient Logging)

Baue mit @svenruppert & sichere Datei-Apps – inkl. NIO, Logging & Security-Fokus: javapro.io/de/erstellen-einer-

Sven Ruppertsvenruppert
2025-06-03

for my german reading follower.. published a post about CWE-22 CWE-377 and CWE-778 and how to harden against it. The demo is written in @Vaadin Flow. javapro.io/de/erstellen-einer-

Stefan Böhringerdatenschauer
2025-05-22
2025-05-14

Jmix/Spring-приложение в IFrame

Предположим, у нас появилась задача встроить какой-то функционал, реализуемый системой на Jmix/Vaadin/Spring на другой сайт или в веб-приложение. Сейчас существует большое количество статических генераторов и других систем управления содержимым, где у разработчика есть доступ только к фронтенд-части. Если это не портальная система, обычным решением в таких случаях будет использовать встраивание через IFrame. Для того чтобы приложение с интерфейсом на Vaadin открывалось в айфрейме за пределами локалхоста, ему требуется включенная поддержка cookie, что по современным стандартам безопасности возможно только если и сайт и приложение, находящиеся на разных доменах, работают по протоколу HTTPS доверенного уровня и для сессионных кук включен параметр Secure и выключен SameSite. Поэтому нам придется немного заморочиться, что бы это все заработало в Spring Boot-приложении даже если речь идет о тестовых средах.

habr.com/ru/companies/haulmont

#iframe #iframeприложения #java #spring #vaadin #jmix

Nicolas Fränkel 🇺🇦🇬🇪frankel@mastodon.top
2025-05-12

My friend Stefano Fago just noticed that my #Vaadin books on @packtpublishing are no longer available in their online catalog.

End of an era 😢

JCONjcon
2025-04-30

? ? ? ? Nah, just give us Java.

At Matti Tahvonen & Florian Habermann go all-in on a clean, full-stack setup using + —no front-end chaos required.

In one live demo, they’ll build a full web app from persistence to UI using just Java. Yes, really. One language to rule them all. 🧙‍♂️

🎟️ 2025.europe.jcon.one/tickets

JCONjcon
2025-03-29

Excited for EUROPE 2025? See Matti Tahvonen & Florian Habermann at in Cologne talking about 'All in on : Simplifying Full-Stack Web with and '

often juggle multiple languages, libraries, and frameworks …

Get your free Ticket: jcon.one

2025-03-18

Welcome @apus in the #Fediverse! #Apus is a #free and #opensource #socialmedia wall for conferences written in #Java using #Vaadin #Flow and #Spring. You can see it in action at the @voxxedzurich on March 25th and @JavaLandConf from April 1st to 3rd!

Nicolas DelsauxRiduidel@framapiaf.org
2025-03-05

Si un jour vous faites du Vaadin, ce que je ne vous souhaite pas, cet article explique bien comment les sessions vaadin sont supprimées. mvysny.github.io/vaadin-sessio #vaadin #session #tomcat #article

2025-02-27

📦 New release: CrudUI Add-on 7.2.0
👉 vaadin.com/directory/component
#vaadin

CrudUI Add-on for Vaadin version 7.2.0 - release notes.
2025-02-24

Remember the "Reindeer" #Vaadin theme?
(Excerpt from my book "Vaadin 7 UI Design by Example" from 2013)

Simon MartinelliSimonMartinelli
2025-01-29

Willst Du Vaadin kennenlernen? Am 3. April hast Du die Gelegenheit im Rahmen der JavaLand Konferenz meine Vaadin Workshop zu besuchen. Melde Dich an 👇

buff.ly/4gmKR5T

Simon MartinelliSimonMartinelli
2025-01-28

In my new blog post, "Securing Vaadin Applications With One-Time Token," I show you how to use the new Spring Security feature that enables the user to log in with a one-time token combined with Vaadin.

buff.ly/3WFsUsn

Simon MartinelliSimonMartinelli
2025-01-27

Securing Vaadin Applications With Microsoft Entra

buff.ly/3POBAZE

2025-01-03

🇪🇸 En Español!

La Evolución del Código Abierto: En esta charla hablo sobre cómo el #software de código abierto—que está en sus dispositivos móviles, #LLMs, #supercomputadores, sistemas operativos y aplicaciones web—ha triunfado y cómo esto es posible por medio de licencias y modelos de negocio alrededor de ellas.

youtube.com/watch?v=9tBpZUt7D6U

#MariaDB #Vaadin #Java #AI #Mistral #MySQL #Linux #

Client Info

Server: https://mastodon.social
Version: 2025.04
Repository: https://github.com/cyevgeniy/lmst