http-eval
v0.0.3
Published
Eval HTTP POST bodies in NodeJS
Downloads
21
Readme
http-eval
Runs a simple http server on a Unix domain socket, which evals POSTed content inside NodeJS.
$ npx http-eval --udsPath /tmp/foo.sock
and 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
42
This is intended for use as a subprocess "sidecar" to execute JavaScript from non-JavaScript programs.
Security stance
A server which eval
s 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
code
which 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
this
context- ... and thus must
return
anything it wants to send back to the client.- ... and any returned values must be
JSON.stringify
able.
- ... 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
this
between calls.
- ... and thus must
- Code can be run within an
async
function (and thus useawait
) using theasync=true
query 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",
[...]
},
[...]
]
}