Vulnerability and remote execution
If you’ve used npm to manage your code dependencies, you’ve used CouchDB more times than you imagine.
CouchDB is a non-relational “NoSQL” database whose main purpose is to replicate data across multiple devices as easily as possible. It's essentially a KVS storage source for saving JSON blobs (documents), with features like data validation, querying, and built-in user authentication. This way, we have a comprehensive API for managing our data with built-in authentication, all running in a single instance, with the ability to replicate and synchronize with other instances or compatible environments in a fast, simple, and automated way.
But we’re not here to go into further detail. If you want to learn more about CouchDB and other ways to store data, here’s a talk we gave about client-side storage that better summarizes this topic and compares it to other traditional ways of persisting data.
What we’re here to tell you about today is an exploit in CouchDB that allows a malicious (or idle) user to execute code on a production instance.
How do they achieve this?
Well, CouchDB was originally written in Erlang, but it allows specifying document validations in JavaScript. These scripts are automatically evaluated whenever a document is created or updated in the database. They run in a new process and are available immediately after being serialized to JSON format from Erlang. The issue lies in how CouchDB handles user accounts via a special table called _users
. Every time you create or modify a user, the server checks your changes with a JavaScript function validate_doc_update
that ensures you’re not attempting unauthorized actions, like registering yourself as an admin user.
Who said… "bugs"?
Okay, but how do they actually do it… What’s the vulnerability then?
The problem is a discrepancy between the JSON parser in JavaScript (used for script validation) and the parser internally used by CouchDB, called jiffy
, which serializes data before handing it off to JavaScript. When you try to parse an object with duplicate keys, for example {"foo":"bar", "foo":"baz"}
, here’s what happens:
Erlang:>> jiffy:decode("{\"foo\":\"bar\", \"foo\":\"baz\"}").
{[{<<"foo">>,<<"bar">>},{<<"foo">>,<<"baz">>}]}
JavaScript:>> JSON.parse("{\"foo\":\"bar\", \"foo\": \"baz\"}")
{foo: "baz"}
For a given key, the Erlang parser keeps all values, but the JavaScript parser only keeps the last repeated value. Unfortunately, CouchDB’s internal function that retrieves these values only returns the first value:
% Within couch_util:get_value
lists:keysearch(Key, 1, List).
This allows attackers to bypass critical checks and create an admin user as follows:
Internally, Erlang grants admin permissions, while on the JavaScript side, the roles[]
appears empty, showing no special permissions. Unfortunately for the defender, all significant authentication logic takes place in Erlang.
Now that we have an admin user, we gain full control over the entire database. The most curious part of this exploit is that it’s not easy to detect via the admin interface (commonly known as Fauxton), since when you investigate the user you just created, the roles
field appears empty, as if it had no special permissions—because on this side, the parsing is done in JavaScript!
What can we do to prevent this?
Update to CouchDB version 2.2 or later to address this bug. This resolves the inconsistency between parsers and prevents this issue.
Override the native behavior of the
_users
database. By default, anonymous users can create accounts in this database. To restrict this, modify the internalvalidate_doc_update
function of the_design/_auth
document. The check!is_server_or_database_admin(userCtx, secObj)
already exists; you just need to use it. To allow only admin users to create new accounts, add the following exception:Add it at the start of the check to block any further actions when the user is not an admin.
The updated
validate_doc_update
function will look like this:Be cautious when using Docker to create production instances. Some repositories allow you to create Docker images with pre-existing hidden vulnerabilities. For CouchDB, the repository clearly exposes the mentioned vulnerability, and here’s how a script automatically creates a user named ‘wooyun.’ If you find strange users unexpectedly created in the DB, it means your image is infected with these vulnerabilities.
¡See you next time!