diff --git a/docs/content/manual/manual.yml b/docs/content/manual/manual.yml index f2000f6ffa..60d8cb77a3 100644 --- a/docs/content/manual/manual.yml +++ b/docs/content/manual/manual.yml @@ -291,6 +291,17 @@ sections: Another way to set the exit status is with the `halt_error` builtin function. + * `--sandbox`: + + Prevent the use of modules (`import`/`include`) 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. + + 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 @@ -2013,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 efa5aa2f34..825949870c 100644 --- a/jq.1.prebuilt +++ b/jq.1.prebuilt @@ -1,5 +1,5 @@ . -.TH "JQ" "1" "March 2024" "" "" +.TH "JQ" "1" "April 2024" "" "" . .SH "NAME" \fBjq\fR \- Command\-line JSON processor @@ -223,6 +223,15 @@ Sets the exit status of jq to 0 if the last output value was neither \fBfalse\fR Another way to set the exit status is with the \fBhalt_error\fR builtin function\. . .TP +\fB\-\-sandbox\fR: +. +.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: . .IP @@ -2189,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 3d2ae0e089..f107084d23 100644 --- a/src/execute.c +++ b/src/execute.c @@ -41,6 +41,7 @@ struct jq_state { unsigned next_label; int halted; + int sandbox; jv exit_code; jv error_message; @@ -1066,6 +1067,7 @@ jq_state *jq_init(void) { jq->curr_frame = 0; jq->error = jv_null(); + jq->sandbox = 0; jq->halted = 0; jq->exit_code = jv_invalid(); jq->error_message = jv_invalid(); @@ -1244,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); @@ -1321,6 +1323,14 @@ void jq_get_stderr_cb(jq_state *jq, jq_msg_cb *cb, void **data) { *data = jq->stderr_cb_data; } +void jq_set_sandbox(jq_state *jq) { + jq->sandbox = 1; +} + +int jq_is_sandbox(jq_state *jq) { + return jq->sandbox; +} + void jq_halt(jq_state *jq, jv exit_code, jv error_message) { diff --git a/src/jq.h b/src/jq.h index 8e9a7b8cf8..54db69a022 100644 --- a/src/jq.h +++ b/src/jq.h @@ -30,6 +30,8 @@ void jq_start(jq_state *, jv value, int); jv jq_next(jq_state *); void jq_teardown(jq_state **); +void jq_set_sandbox(jq_state *); +int jq_is_sandbox(jq_state *); void jq_halt(jq_state *, jv, jv); int jq_halted(jq_state *); jv jq_get_exit_code(jq_state *); diff --git a/src/linker.c b/src/linker.c index e7d1024c1d..38ae78f128 100644 --- a/src/linker.c +++ b/src/linker.c @@ -251,6 +251,15 @@ static int process_dependencies(jq_state *jq, jv jq_origin, jv lib_origin, block i--; jv dep = jv_array_get(jv_copy(deps), i); + // Loading dependencies is not allowed when running in sandbox mode. + if (jq_is_sandbox(jq)) { + jq_report_error(jq, jv_string("jq: error: Loading dependencies (with import or include) is not allowed in sandbox mode")); + jv_free(deps); + jv_free(jq_origin); + jv_free(lib_origin); + return 1; + } + const char *as_str = NULL; int is_data = jv_get_kind(jv_object_get(jv_copy(dep), jv_string("is_data"))) == JV_KIND_TRUE; int raw = 0; @@ -420,14 +429,16 @@ int load_program(jq_state *jq, struct locfile* src, block *out_block) { return 1; } - char* home = getenv("HOME"); - if (home) { // silently ignore no $HOME - /* Import ~/.jq as a library named "" found in $HOME */ - block import = gen_import_meta(gen_import("", NULL, 0), - gen_const(JV_OBJECT( - jv_string("optional"), jv_true(), - jv_string("search"), jv_string(home)))); - program = BLOCK(import, program); + if (!jq_is_sandbox(jq)) { + char* home = getenv("HOME"); + if (home) { // silently ignore no $HOME + /* Import ~/.jq as a library named "" found in $HOME */ + block import = gen_import_meta(gen_import("", NULL, 0), + gen_const(JV_OBJECT( + jv_string("optional"), jv_true(), + jv_string("search"), jv_string(home)))); + program = BLOCK(import, program); + } } nerrors = process_dependencies(jq, jq_get_jq_origin(jq), jq_get_prog_origin(jq), &program, &lib_state); diff --git a/src/main.c b/src/main.c index 832330802e..a29f55bdc6 100644 --- a/src/main.c +++ b/src/main.c @@ -106,6 +106,7 @@ static void usage(int code, int keep_it_short) { " --jsonargs consume remaining arguments as positional\n" " JSON values;\n" " -e, --exit-status set exit status code based on the output;\n" + " --sandbox prevent dynamic access to other files/data;\n" #ifdef WIN32 " -b, --binary open input/output streams in binary mode;\n" #endif @@ -475,6 +476,10 @@ int main(int argc, char* argv[]) { parser_flags |= JV_PARSE_STREAMING | JV_PARSE_STREAM_ERRORS; continue; } + if (isoption(argv[i], 0, "sandbox", &short_opts)) { + jq_set_sandbox(jq); + continue; + } if (isoption(argv[i], 'e', "exit-status", &short_opts)) { options |= EXIT_STATUS; if (!short_opts) continue; diff --git a/tests/shtest b/tests/shtest index 6cc2e1725b..f7dbf8582d 100755 --- a/tests/shtest +++ b/tests/shtest @@ -381,6 +381,43 @@ if ! $VALGRIND $Q $JQ -L tests/modules -ne 'import "test_bind_order" as check; c exit 1 fi +if HOME="$mods/home1" $VALGRIND $Q $JQ --sandbox -nr fg; then + echo "home module was loaded when it should have been prevented by sandbox flag" 1>&2 + exit 1 +fi + +if HOME="$mods/home2" $VALGRIND $Q $JQ --sandbox -n 'include "g"; empty'; then + echo "module was included when it should have been prevented by sandbox flag" 1>&2 + exit 1 +fi + +if $VALGRIND $Q $JQ -L ./tests/modules --sandbox -n 'import "a" as a; empty'; then + echo "module was imported when it should have been prevented by sandbox flag" 1>&2 + 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