From c39f44764855006b96d17c84337eb043beb10fe8 Mon Sep 17 00:00:00 2001 From: Joe Eli McIlvain Date: Tue, 16 Apr 2024 12:45:00 -0700 Subject: [PATCH] Update `--sandbox` flag behavior to clear environment variables. In a security-sensitive environment where the `--sandbox` flag can be used to mitigate some categories of threats from untrusted filter code and/or untrusted JSON data, it is also desirable to prevent leaking environment variable values (which often can include secrets in some environments). This commit does so by updating the behavior of `--sandbox` to also clear the environment variables seen by the jq filter code in the `$ENV` value and `env` builtin. --- docs/content/manual/manual.yml | 8 ++++++++ jq.1.prebuilt | 6 ++++++ src/builtin.c | 5 +++++ src/compile.c | 10 ++++++++-- src/compile.h | 2 +- src/execute.c | 2 +- tests/shtest | 22 ++++++++++++++++++++++ 7 files changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/content/manual/manual.yml b/docs/content/manual/manual.yml index 75580de2f5..60d8cb77a3 100644 --- a/docs/content/manual/manual.yml +++ b/docs/content/manual/manual.yml @@ -297,6 +297,11 @@ sections: operations that would allow the filter code to access data other than the input data that is explicitly specified in the invocation. + This flag also hides all environment variables from the enviroment + where jq was run by setting `$ENV` and `env` to be an empty object. + If you need to pass named arguments to a sandboxed jq filter, use the + `--arg` and/or `--argjson` options to pass them explicitly. + * `--binary` / `-b`: Windows users using WSL, MSYS2, or Cygwin, should use this option @@ -2019,6 +2024,9 @@ sections: `env` outputs an object representing jq's current environment. + `$ENV` and `env` will be an empty object if jq was run with the + `--sandbox` flag. + At the moment there is no builtin for setting environment variables. diff --git a/jq.1.prebuilt b/jq.1.prebuilt index d5c34baaef..825949870c 100644 --- a/jq.1.prebuilt +++ b/jq.1.prebuilt @@ -228,6 +228,9 @@ Another way to set the exit status is with the \fBhalt_error\fR builtin function .IP Prevent the use of modules (\fBimport\fR/\fBinclude\fR) or any other file operations that would allow the filter code to access data other than the input data that is explicitly specified in the invocation\. . +.IP +This flag also hides all environment variables from the enviroment where jq was run by setting \fB$ENV\fR and \fBenv\fR to be an empty object\. If you need to pass named arguments to a sandboxed jq filter, use the \fB\-\-arg\fR and/or \fB\-\-argjson\fR options to pass them explicitly\. +. .TP \fB\-\-binary\fR / \fB\-b\fR: . @@ -2195,6 +2198,9 @@ Note that this can be overriden in the command\-line with \fB\-\-arg\fR and rela \fBenv\fR outputs an object representing jq\'s current environment\. . .P +\fB$ENV\fR and \fBenv\fR will be an empty object if jq was run with the \fB\-\-sandbox\fR flag\. +. +.P At the moment there is no builtin for setting environment variables\. . .IP "" 4 diff --git a/src/builtin.c b/src/builtin.c index ebc1863d47..0485186609 100644 --- a/src/builtin.c +++ b/src/builtin.c @@ -1137,6 +1137,11 @@ extern char **environ; static jv f_env(jq_state *jq, jv input) { jv_free(input); jv env = jv_object(); + + // A sandboxed filter doesn't have access to environment variables, + // so in such a case we return the empty object without using environ. + if (jq_is_sandbox(jq)) return env; + const char *var, *val; for (char **e = environ; *e != NULL; e++) { var = e[0]; diff --git a/src/compile.c b/src/compile.c index e5e65f2014..8d77b1d4dc 100644 --- a/src/compile.c +++ b/src/compile.c @@ -1367,7 +1367,7 @@ static int compile(struct bytecode* bc, block b, struct locfile* lf, jv args, jv return errors; } -int block_compile(block b, struct bytecode** out, struct locfile* lf, jv args) { +int block_compile(block b, struct bytecode** out, struct locfile* lf, jv args, int is_sandbox) { struct bytecode* bc = jv_mem_alloc(sizeof(struct bytecode)); bc->parent = 0; bc->nclosures = 0; @@ -1377,7 +1377,13 @@ int block_compile(block b, struct bytecode** out, struct locfile* lf, jv args) { bc->globals->cfunctions = jv_mem_calloc(ncfunc, sizeof(struct cfunction)); bc->globals->cfunc_names = jv_array(); bc->debuginfo = jv_object_set(jv_object(), jv_string("name"), jv_null()); - jv env = jv_invalid(); + + // When sandboxed, we don't want to expose environment vars to the program, + // so we create an empty object which is already valid. This prevents a + // later step from creating a populated `$ENV` object, because that step + // only does so if the current value for `env` is invalid. + jv env = is_sandbox ? jv_object() : jv_invalid(); + int nerrors = compile(bc, b, lf, args, &env); jv_free(args); jv_free(env); diff --git a/src/compile.h b/src/compile.h index c1512e6b87..db3fa28b71 100644 --- a/src/compile.h +++ b/src/compile.h @@ -79,7 +79,7 @@ block block_drop_unreferenced(block body); jv block_take_imports(block* body); jv block_list_funcs(block body, int omit_underscores); -int block_compile(block, struct bytecode**, struct locfile*, jv); +int block_compile(block, struct bytecode**, struct locfile*, jv, int is_sandbox); void block_free(block); diff --git a/src/execute.c b/src/execute.c index 40f18394f4..f107084d23 100644 --- a/src/execute.c +++ b/src/execute.c @@ -1246,7 +1246,7 @@ int jq_compile_args(jq_state *jq, const char* str, jv args) { if (nerrors == 0) { nerrors = builtins_bind(jq, &program); if (nerrors == 0) { - nerrors = block_compile(program, &jq->bc, locations, args2obj(args)); + nerrors = block_compile(program, &jq->bc, locations, args2obj(args), jq_is_sandbox(jq)); } } else jv_free(args); diff --git a/tests/shtest b/tests/shtest index 4e062963aa..f7dbf8582d 100755 --- a/tests/shtest +++ b/tests/shtest @@ -396,6 +396,28 @@ if $VALGRIND $Q $JQ -L ./tests/modules --sandbox -n 'import "a" as a; empty'; th exit 1 fi +## Test environment variable access + +if [ "$(FOO=foo $VALGRIND $Q $JQ -nr '$ENV.FOO')" != foo ]; then + echo "couldn't read an environment variable via \$ENV" 1>&2 + exit 1 +fi + +if [ "$(FOO=foo $VALGRIND $Q $JQ --sandbox -nr '$ENV.FOO')" != null ]; then + echo "\$ENV should have been empty due to the sandbox flag" 1>&2 + exit 1 +fi + +if [ "$(FOO=foo $VALGRIND $Q $JQ -nr 'env.FOO')" != foo ]; then + echo "couldn't read an environment variable via env" 1>&2 + exit 1 +fi + +if [ "$(FOO=foo $VALGRIND $Q $JQ --sandbox -nr 'env.FOO')" != null ]; then + echo "env should have been empty due to the sandbox flag" 1>&2 + exit 1 +fi + ## Halt if ! $VALGRIND $Q $JQ -n halt; then