Tags: security

How to protect aeson code from hash flooding

A few weeks ago Tom Sydney Kerckhove (@kerckhove_ts) published an excellent writeup of a serious DoS vulnerability in aeson, a widely used Haskell JSON library. A new aeson release addresses the hash flooding issue, but you need more than a version bump to ensure your programs are protected. This post outlines how aeson addressed the vulnerability and what action you need to take.

Overview of the issue §

Tom’s article is great and if you want the gory details, go read it. There’s no need for me to repeat it here. It’s enough to say that the attack, called hash flooding or hash DoS, exploits the behaviour of the HashMap implementation from unordered-containers, which aeson used. It results in a denial of service through CPU consumption. This technique has been used in real-world attacks against a variety of languages, libraries and frameworks over the years.

Am I vulnerable? §

If you are using aeson < 2.0.0.0 and processing JSON from untrusted sources, you are probably vulnerable. You could mitigate the attack by refusing to decode large inputs, if your use case allows it. Rate limiting may be a possible mitigation for some applications.

How did aeson address the vulnerability? §

Whereas prior versions used HashMap directly, starting at version 2.0.0.0 aeson abstracts the map implementation behind a new data type: Data.Aeson.KeyMap. The ordered-keymap Cabal flag selects the underlying implementation. When set, aeson uses the Ord-based Map from containers. If unset, aeson uses HashMap.

Version 2.0.0.0 defaults the flag to False. As of 2.0.1.0 it defaults to True. Importantly, the maintainers offer no guarantee that the default won’t change again. So if you use aeson and want to protect yourself from hash flooding attacks, take the extra precautions outlined in the following sections.

This is an API-breaking change, hence the major version bump. Most users will not have to change much code, but there will be exceptions (I had to change quite a lot for jose).

The Map version also behaves differently from HashMap. In particular, objects may be serialised with a different key order, and object keys are iterated in different orders. And who knows what systems out there depend on the key order in some way, even though they should not. That is a big reason why the maintainers felt it was necessary to keep the option of using HashMap.

Also, these data structures have different performance characteristics, with Map having O(log n) insertion and lookup time. HashMap insertion and lookup are amortised O(1), degrading to O(n) for pathological inputs—which is the cause of the vulnerability!

Compiling a safe version of aeson §

If you have a program or library that uses aeson, you need to ensure that the aeson you link against was compiled with the ordered-keymap flag. There is no way to express this condition in a .cabal file, but you can can express these constraints in the cabal.project file:

packages: .
constraints:
  aeson +ordered-keymap

For Stack users, configure the flag in your stack.yaml:

flags:
  aeson:
    ordered-keymap: true

If you’re building and installing aeson directly, via cabal-install (the cabal program), you can use the --flags=ordered-keymap command line option.

Runtime checks §

In your program or library you can also detect the KeyMap implementation at runtime. If you detect HashMap you could abort, emit a warning, or employ other mitigations like limiting the input size.

Data.Aeson.KeyMap exports the following types:

coercionToHashMap
  :: Maybe (Coercion (HashMap Key v) (KeyMap v))

coercionToMap
  :: Maybe (Coercion     (Map Key v) (KeyMap v))

The values are coercions—proofs of representational equality enabling zero-cost conversions; see Data.Type.Coercion. Only one of HashMap or Map is actually used, which is why they’re wrapped in Maybe. The map implementation that aeson is using has a non-Nothing coercion.

In jose I will export the following value to make it easy for library users to check that the implementation is safe from hash flooding:

vulnerableToHashFlood :: Bool
vulnerableToHashFlood =
  case KeyMap.coercionToMap of
    Just _  -> False
    Nothing -> True

Users can (and hopefully will) check that value and respond in whatever way is suitable for their use case. I might go even further and cause all JWS processing to immediately fail when the vulnerable implementation is detected, unless the caller overrides this behaviour.

What about other things that use HashMap? §

The HashMap data structure from unordered-containers remains vulnerable to hash flooding attacks. Users and maintainers are discussion potential solutions and mitigations in issue #319. There are several interesting ideas, including:

This discussion is ongoing. The only change so far is to add a security advisory to the package description.

Conclusion §

aeson >= 2.0.0.0 has mitigated the hash flooding vulnerability. Users of the library must take specific action not only to upgrade aeson to the latest version, but also ensure it is compiled with the correct flags. Programs can also perform runtime checks and take appropriate action if aeson is using HashMap.

Creative Commons License
Except where otherwise noted, this work is licensed under a Creative Commons Attribution 4.0 International License .