Skip to content

Notes ·

JSON Is Cheap to Send and Expensive to Believe

A post on the Full Disclosure mailing list this morning describes an old denial-of-service technique with the wonderfully understated name "Commas of D00m."

The proof of concept targets JSON deserializers by sending them enormous collections containing mostly null values.

That sounds almost too simple.

It is.

That is also why it is interesting.

The attacker does not need malformed JSON, executable code or some exotic parser exploit.

They send something structurally similar to:

{"serviceTypes":[null,null,null,null,null,...]}

except the array is enormous.

Then they do it repeatedly.

The problem is that what is cheap on one side of the connection may be considerably more expensive on the other.

What the server actually has to do

When a web application receives JSON, it usually does not leave the request as a pile of characters.

A JSON deserializer turns that text into objects the application can work with.

Something like:

[null,null,null]

may become an array, list or collection in memory containing three entries.

Make that collection enormous and the parser still has to process every separator, recognize every value, grow or allocate the destination collection, keep track of its contents and eventually hand the resulting object to the application.

The attacker sends characters.

The server creates state.

That difference is the attack.

A comma and the word null cost almost nothing to generate.

Processing a million of them is not necessarily cheap.

Now send several of those requests at the same time.

Memory pressure rises.

CPU time gets consumed parsing garbage.

The garbage collector may start working harder.

Worker threads remain occupied.

Requests from legitimate users wait behind them.

Eventually something may run out of memory, time out or fall over.

No clever remote-code-execution chain is required.

You just persuade the server to work much harder than you did.

The JSON can be completely valid

I think this is the part people outside security may find surprising.

We tend to think of malicious input as input that is somehow invalid.

A broken image.

A malformed packet.

An illegal instruction.

A weird Unicode sequence.

This attack can use completely legitimate JSON.

The problem is not:

Can I parse this?

The problem is:

Should I agree to parse this much of it?

Those are very different questions.

A parser can correctly implement the JSON specification and still expose an application to resource exhaustion if nothing above or below it imposes sensible limits.

That is why input validation cannot stop at checking whether something is syntactically valid.

Valid does not mean reasonable.

The proof of concept is really testing boundaries

The Full Disclosure code creates a very large JSON array and sends copies of it concurrently to a target endpoint.

It watches the responses for things like 413 Payload Too Large, 429 Too Many Requests, and server-side 5xx errors.

Those responses tell you something about where the defenses are.

A 413 is encouraging.

Something rejected the request because it was too large.

A 429 may mean rate limiting noticed the volume.

A 500, 502, 503 or 504 under this kind of load can be much more interesting because it may indicate the request made it far enough into the application stack to start exhausting resources or destabilizing something downstream.

The exact failure depends heavily on the framework and application.

The interesting question is always where the request becomes expensive.

Web server?

Reverse proxy?

JSON parser?

Object mapper?

Application validation?

Database?

Some service called afterward?

Every layer that accepts unbounded work on behalf of an untrusted request becomes part of the attack surface.

This is not the deserialization bug most people think of

"Insecure deserialization" usually makes security people think about gadget chains.

An application accepts serialized data, reconstructs attacker-controlled object types, and some unfortunate combination of reflection and object behavior eventually causes code execution.

Tools such as ysoserial became famous for demonstrating that class of bug.

The authors of this Full Disclosure post are making a somewhat different argument.

Deserialization can be insecure even when no unexpected code gets executed.

Simply allowing an attacker to force the parser to construct absurdly large objects is enough.

I think unconstrained resource consumption during deserialization is the clearer description.

Nothing magical has to happen.

The server just agrees to do far too much work.

This family of attacks has been around for a very long time.

XML had its own famous versions.

Regular expressions can have catastrophic backtracking.

Compression formats can expand tiny inputs into enormous outputs.

Recursive parsers can be driven into ridiculous nesting depths.

Hash tables have been attacked through pathological collision behavior.

The recurring pattern is the same:

Make the victim spend substantially more resources processing the input than the attacker spent creating it.

JSON is simply another place where that asymmetry can appear.

Why null is such a nice weapon

There is something almost elegant about filling the payload with null.

There is very little information in it.

The attacker is not uploading a giant video or sending useful data.

It is mostly punctuation and the absence of values.

Yet the receiver still has to recognize all of it and represent the structure somehow.

It is the computational equivalent of mailing somebody an enormous box containing individually wrapped pieces of nothing.

The box is not particularly interesting.

Opening every package is the problem.

Hence the commas.

Lots and lots of commas.

The right defense is boring

There is no reason an ordinary API should accept arbitrarily large collections just because the programming language technically can.

If an endpoint expects five service types, accepting 500,000 of them is not flexibility.

It is missing validation.

Useful defenses exist at several layers.

Limit request body sizes before they reach the application.

Limit the number of items accepted in collections.

Limit nesting depth.

Put time and memory boundaries around parsing.

Rate-limit expensive endpoints.

Apply backpressure.

Reject unreasonable inputs as early as possible.

Monitor how much work a request causes, not merely how many bytes arrived over the network.

And, most importantly, decide what reasonable means for the application.

A generic JSON parser cannot know that an array called serviceTypes should probably contain fewer items than the population of Sacramento.

The application does.

That is where the constraint ultimately belongs.

The uncomfortable lesson is how ordinary the attack is

There is no zero-day here.

No shellcode.

No mysterious CPU instruction.

No cinematic moment where somebody types really quickly and says "I'm in."

It is a web request containing an unreasonable number of commas.

That is part of what makes these bugs so easy to overlook.

Developers spend a lot of time thinking about whether input contains something dangerous.

Sometimes the dangerous property is simply how much of it we agreed to process.

The Full Disclosure authors say they have been using variations of this technique against Struts2, Newtonsoft JSON, JSON.org and other parsers for more than 15 years.

That longevity does not surprise me.

Frameworks change.

Languages change.

Serialization formats change.

The underlying mistake is remarkably durable:

Accept untrusted input.

Perform unbounded work.

Hope nobody notices.

Someone always eventually notices.

And sometimes all they need is a comma.

All notes