http-eval v0.0.3
http-eval
Runs a simple http server on a Unix domain socket, which evals POSTed content inside NodeJS.
$ npx http-eval --udsPath /tmp/foo.sockand then:
$ curl \
--silent \
--unix-socket /tmp/foo.sock \
'http://bogus/run' \
-X POST \
-H 'Content-Type: application/json' \
-d '{ "code": "return 6*7;"}' \
| jq .result
42This is intended for use as a subprocess "sidecar" to execute JavaScript from non-JavaScript programs.
Security stance
A server which evals arbitrary JavaScript obviously raises security concerns!
If constructed incorrectly, this would essentially be a vector for Remote Code
Execution (RCE) attack.
http-eval attempts to guarantee that only the current local Unix user, and
folks in its Unix group, can send JavaScript to the server.
Note that, as stated in the LICENSE file, http-eval has no warranty of any
kind guaranteeing the above security stance. Use at your own risk. Please
report any discovered faults using Github issues!
To do this, http-eval only listens on a Unix domain socket. As constructed, it
refuses to bind to a TCP/IP interface at all.
Unix domain sockets are ordinarily located in the Unix filesystem, and access is controlled by ordinary filesystem permissions. Thus, only programs which can write to local files can send JavaScript code to the server.
Now, there may be different trust boundaries within a machine and its
filesystem! Usually, we don't want one local (non-root) Unix user to have
access to run arbitrary commands as another local Unix user (typically we'd call
that a privilege escalation vulnerability). To mitigate the risk of such a
misconfiguration, by default http-eval validates the file permissions,
ensuring that the UDS does not have world-write access. (However, group-write
is still allowed.) http-eval effectively mandates a umask of at least
0002.
This file permission check is simply a best-effort attempt to detect a configuration footgun. This does not provide any security guarantee against misconfiguration.
To intentionally disable the file permission check, set the option
--ignoreInsecureSocketPermission. This could plausibly be useful in situations
where you are using other guarantees (e.g., directory permissions, or chroot) to
protect write access to the UDS, and/or you actually intend to enable local
privilege escalation via http-eval. Obviously, disable this check at your own
risk.
Details
- Requests must:
- contain a JSON body (
Content-Type: application/json)- ... with an object containing the key
codewhich contains the code to execute.
- ... with an object containing the key
- accept JSON in UTF-8 (
Accept-Encoding: application/json,Accept-Charset: UTF-8).
- contain a JSON body (
- Responses contain UTF-8 JSON with:
- the result in the object key
result, or - any exception in the object key
error.
- the result in the object key
- Code is evaluated as a function body within an ECMAScript module with a
consistent
thiscontext- ... and thus must
returnanything it wants to send back to the client.- ... and any returned values must be
JSON.stringifyable.
- ... and any returned values must be
- ... and thus can use
dynamic
await import(...)but notrequire(and import is generally best used in async mode; see below). - ... and thus may store values on
thisbetween calls.
- ... and thus must
- Code can be run within an
asyncfunction (and thus useawait) using theasync=truequery parameter- ... and as noted above, this is required for use of
await import(...).
- ... and as noted above, this is required for use of
Examples
Basic sync call
$ curl \
--silent \
--unix-socket /tmp/foo.sock \
'http://bogus/run' \
-X POST \
-H 'Content-Type: application/json' \
-d '{ "code": "return 6*7;"}' \
| jq .
{
"result": 42
}Basic error
$ curl \
--silent \
--unix-socket /tmp/foo.sock \
'http://bogus/run' \
-X POST \
-H 'Content-Type: application/json' \
-d '{ "code": "bad code;"}' \
| jq .
{
"error": "HttpEvalError: Error in eval\n [...]",
"cause": {
"error": "SyntaxError: Unexpected identifier 'code'\n at Function (<anonymous>)\n at [...]"
}
}Storing and retrieving values on this
$ curl \
--silent \
--unix-socket /tmp/foo.sock \
'http://bogus/run' \
-X POST \
-H 'Content-Type: application/json' \
-d '{ "code": "this.foo = 6*7;"}' \
| jq .
{}
$ curl \
--silent \
--unix-socket /tmp/foo.sock \
'http://bogus/run' \
-X POST \
-H 'Content-Type: application/json' \
-d '{ "code": "return this.foo;"}' \
| jq .
{
"result": 42
}Basic async call
$ curl \
--silent \
--unix-socket /tmp/foo.sock \
'http://bogus/run?async=true' \
-X POST \
-H 'Content-Type: application/json' \
-d '{ "code": "await new Promise(resolve => setTimeout(resolve, 2000));"}' \
| jq .
{}Async call with a dynamic import
$ curl \
--silent \
--unix-socket /tmp/foo.sock \
'http://bogus/run?async=true' \
-X POST \
-H 'Content-Type: application/json' \
-d '{ "code": "let os = await import(\"os\"); return os.cpus();"}' \
| jq .
{
"result": [
{
"model": "DMC(R) DeLorean(R) DMC-12 @ 1.21 GW",
[...]
},
[...]
]
}