CouchDB Authentication Without Server-Side Code

This is a guest post by community member Eirik Brandtzæg.

The powerful REST API in CouchDB makes building client-side only web applications a breeze.
It’s also possible to add security by requiring users to authenticate against the API. Here we will discuss how to authenticate against the API client-side, or via nginx, without any server-side code.

Enable authentication

Enable authentication by setting require_valid_user to true, located under Configuration > Main config > chttpd. With the setting enabled every request to CouchDB must either contain a valid Authentication or Cookie header, a simple yet effective security mechanism.

Client-side authentication

The best way for a client to login is to retrieve a cookie, which is done by making a POST against /_session with name and password in body, like this:
(note; won’t work)

POST /_session
Content-Type: application/json

{"name":"admin","password":"admin"}

However, it’s a bit more challenging, because the request making the POST must also include a valid Authroization or Cookie header for the user we want to authenticate.
The only way to properly receive a Cookie is to include the Authorization header, so the request using Basic authentication must look like this:

POST /_session
Content-Type: application/json
Authorization: Basic YWRtaW46YWRtaW4=

{"name":"admin","password":"admin"}

With curl this could be achieved by the following command curl http://admin:admin@localhost:5984/_session -H 'Content-Type: application/json' -d '{"name":"admin","password":"admin"}'.

In a client-side web application we can achieve this by appending the header to the request, like this:

fetch('/_session', {
  method: 'POST',
  credentials: 'include',
  headers: {
    'content-type': 'application/json',
    authorization: `Basic ${btoa('admin:admin')}`
  },
  body: JSON.stringify({name: 'admin', password: 'admin'})
})

Now every subsequent request only need to include credentials: 'include'.
This solution also works cross-domain, by enabling CORS under Configuration > CORS, even with All domains (*) as origin.
It’s also possible to do all requests using the Authorization header, but the Cookie-approach is much cleaner and safer.

Project Fauxton

One of the immediate drawbacks of requiring valid users is that /_utils (Project Fauxton) is essentially unreachable, as the login-form is also locked down.

Logging in via the client-side approach above is possible, if the web application is hosted on the same origin (domain) as CouchDB, or by running the above fetch-code directly in the browser DevTools console.

nginx and proxy_pass

Another more sleek solution is to add nginx as a proxy, and render a custom login-form when CouchDB requires authentication. Any other proxy/gateway can also be used.

We can set nginx to proxy all requests to CouchDB, and tell it to intercept all errors. When intercepting errors nginx will show its own error pages, and then we can create our own 401 page, which is a login form.
Creating a custom path, e.g., /login, is also possible.
The config could look like this:

server {
  listen 80;
  
  location ~ 401.html$ {
    alias /usr/share/nginx/html/couchdb/login.html;
  }

  location / {
    error_page 401 /401.html;
    proxy_intercept_errors on;
    proxy_pass http://172.17.0.1:5984;
  }
}

Then we can make a custom login form as login.html.
Note, normal form with action won’t work, again because of the required Authorization header, but we can use the JavaScript snippet from above to bypass this:

<!doctype html>
<title>LOGIN!</title>
<form id="form">
  <input id="username" placeholder="name">
  <input id="password" placeholder="password" type="password">
  <button type="submit">LOGIN!</button>
  <p id="output"></p>

  <a href="/_utils">Go to Project Fauxton</a>
</form>
<script>
  form.addEventListener('submit', e => {
    e.preventDefault();
    const data = {name: username.value, password: password.value};
    output.innerText = '';
    fetch('/_session', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'content-type': 'application/json', 
        authorization: `Basic ${btoa(data.name + ':' + data.password)}`},
      body: JSON.stringify(data)
    }).then(res => res.text()).then(res => output.innerText = res);
  });
</script>

Apache CouchDB CVE-2017-12635 and CVE-2017-12636

Last week, we announced the release of CouchDB versions 2.1.1 & 1.7.0/1.7.1 and marked them as CRITICAL security updates.

Today we are releasing detailed information about the security issues.

We expect all users to have updated already.

Overview

CVE-2017-12635

Due to differences in CouchDB’s Erlang-based JSON parser and JavaScript-based JSON parser, it is possible to submit _users documents with duplicate keys for roles used for access control within the database, including the special case _admin role, that denotes administrative users. In combination with CVE-2017-12636 (Remote Code Execution), this can be used to give non-admin users access to arbitrary shell commands on the server as the database system user.

The JSON parser differences result in behaviour that if two roles keys are available in the JSON, the second one will be used for authorising the document write, but the first roles key is used for subsequent authorisation for the newly created user. By design, users can not assign themselves roles. The vulnerability allows non-admin users to give themselves admin privileges.

We addressed this issue by updating the way CouchDB parses JSON in Erlang, mimicking the JavaScript behaviour of picking the last key, if duplicates exist.

This issue was discovered by Max Justicz.

See also: Max’s own blog post about the issue and the motivation behind his research.

CVE-2017-12636

CouchDB administrative users can configure the database server via HTTP(S). Some of the configuration options include paths for operating system-level binaries that are subsequently launched by CouchDB. This allows a CouchDB admin user to execute arbitrary shell commands as the CouchDB user, including downloading and executing scripts from the public internet.

This issue was discovered by Joan Touzet of the CouchDB Security team during the investigation of CVE-2017-12635.

Details on CVE-2017-12635

CouchDB’s data exchange format is exclusively JSON. Internally, JSON is represented as an Erlang proplist. That implementation detail leads to a nearly unique behaviour in CouchDB when it comes to JSON parsing:: allowing JSON documents with multiple keys of the same name.

CouchDB uses JavaScript-based authorisation functions for managing users’ access control. For example, to create a user, a document is inserted into the special-case _users database. The user document must include a top level key roles with a list of strings that gives users extra privileges.

By default, only admin users can set roles, ensuring users themselves can not exceed their assigned privileges. In addition, it is possible to let users sign up anonymously. In that scenario, it is even more important that privilege-granting roles are not self-selected.

The JavaScript spec for JSON with multiple keys requires parsers to pick the last key they parse to be the one to end up in the JavaScript object constructed from that JSON string.

CouchDB internal and Erlang-level accessor functions pick the first available key.

This difference led to multiple exploit scenarios:

  1. via _users
  2. via _replicator

In the case of the _users database, allowing duplicate keys allowed attackers to submit a _users doc with two roles keys. The first one could include any self-assigned roles, as the JavaScript validation function would only ever see the second key. When authenticating users on subsequent requests, the roles key was read from the Erlang-JSON representation and in that implementation the first roles key was returned. That could now include any roles, including the special case _admin role, giving users admin privileges.

In the case of the _replicator database, the authentication bypass occurs because the CouchDB replicator assumes it is the only actor that can set the _replication_state field of a replication document. The document validation skips further authentication if that field has a value of false. The duplicate keys bypass now allows users to set the _replication_state field to false to enable arbitrary edits, and then allows to modify the user context (userCtx) of the replication arbitrarily, which then can be used to insert documents into any database. This includes _design documents which could hold further validation and authentication functions that could be disabled that way.

Details on CVE 2017-12636

CouchDB allows the setting of configuration variables via HTTP(S) or by editing configuration files. Changing via HTTP(S) has the advantage of avoiding a process restart. A number of CouchDB’s configuration settings are file paths to executables that are dutifully executed by CouchDB proper on startup or when a new config setting is provided via HTTP(S).

The config API is only available to admin users. Setting arbitrary executable paths allows admins to set any executable through the use of the HTTP(S) API. These executables will run in the scope of the operating system user under which CouchDB runs. This includes read/write access to the database files, and at least read access to its configuration files.

We have mitigated this attack-vector by implementing a blacklist ofconfiguration settings that can not be changed via HTTP(S) and require shell access to modify.

A hard-coded blacklist has the downside of needing to be updated when newly added configuration settings to CouchDB also need blacklisting. We opted for this approach for reasons of expediency. In future, we’ll extend the configuration system that the addition of configuration settings will require explicit marking a variable to be safe for editing via HTTP(S).

In combination with CVE-2017-12635, this allows anonymous users to gain shell access as the operating system user that CouchDB runs under.

Am I impacted?

The easiest way to find out whether you have been targeted with documents that include duplicate keys is to run this command to check the _users database:

curl -s 'admin:password@127.0.0.1:5984/_users/_all_docs?include_docs=true' | grep -E '"roles".+"roles"'

Analogous for _replicator:

curl -s 'admin:password@127.0.0.1:5984/_users/_all_docs?include_docs=true' | grep -E '"_replication_state".+"_replication_state"'

Impact Assessment

Due to the severity of these issues and their combination, we have delayed the release of this detailed information, and urged all users to update to the latest releases.

If you still haven’t updated, here’s a short guide to asses whether the vulnerabilities affect you. This is valid for all CouchDB 1.x and 2.x users that aren’t on 2.1.1 or later or 1.7.0/1 or later.

  • Public CouchDB instances:
    • you should already have updated to the new release.
    • if you have enabled require_valid_user AND if you trust all your users with database admin & server shell access: there is no problem
  • Internal CouchDB instances:
    • if you trust everyone on the network: no problem.
    • if you have require_valid_user enabled AND if you trust all your users with database admin & server shell access: no problem
    • if you have require_valid_user disabled AND if you trust all your users with database admin & server shell access: enable require_valid_user