diff --git a/docs/content/manual/manual.yml b/docs/content/manual/manual.yml index f2000f6ffa..75580de2f5 100644 --- a/docs/content/manual/manual.yml +++ b/docs/content/manual/manual.yml @@ -291,6 +291,12 @@ 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. + * `--binary` / `-b`: Windows users using WSL, MSYS2, or Cygwin, should use this option diff --git a/jq.1.prebuilt b/jq.1.prebuilt index efa5aa2f34..d5c34baaef 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,12 @@ 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\. +. +.TP \fB\-\-binary\fR / \fB\-b\fR: . .IP diff --git a/src/execute.c b/src/execute.c index 3d2ae0e089..40f18394f4 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(); @@ -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..4e062963aa 100755 --- a/tests/shtest +++ b/tests/shtest @@ -381,6 +381,21 @@ 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 + ## Halt if ! $VALGRIND $Q $JQ -n halt; then