Repository navigation
fix(task): require trust for remote metadata #12202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
59b16ab
ea1073c
a5e794d
5177f40
814d3a3
970fa76
b96801d
295b835
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,15 +8,17 @@ PORT_FILE="$TMPDIR/mise_git_http_port" | |||||||||||||||||||||
| READY_FILE="$TMPDIR/mise_git_http_ready" | ||||||||||||||||||||||
| INFO_FILE="$TMPDIR/mise_git_http_info" | ||||||||||||||||||||||
| UPLOAD_PACK_COUNT_FILE="$TMPDIR/mise_git_http_upload_pack_count" | ||||||||||||||||||||||
| REQUEST_LOG="$TMPDIR/mise_git_http_requests" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Clean up any previous server files | ||||||||||||||||||||||
| rm -f "$PORT_FILE" "$READY_FILE" "$INFO_FILE" "$UPLOAD_PACK_COUNT_FILE" | ||||||||||||||||||||||
| rm -f "$PORT_FILE" "$READY_FILE" "$INFO_FILE" "$UPLOAD_PACK_COUNT_FILE" "$REQUEST_LOG" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Start local git HTTP server with OS-assigned port to avoid race conditions | ||||||||||||||||||||||
| MISE_GIT_HTTP_PORT_FILE="$PORT_FILE" \ | ||||||||||||||||||||||
| MISE_GIT_HTTP_READY_FILE="$READY_FILE" \ | ||||||||||||||||||||||
| MISE_GIT_HTTP_INFO_FILE="$INFO_FILE" \ | ||||||||||||||||||||||
| MISE_GIT_HTTP_UPLOAD_PACK_COUNT_FILE="$UPLOAD_PACK_COUNT_FILE" \ | ||||||||||||||||||||||
| MISE_GIT_HTTP_REQUEST_LOG="$REQUEST_LOG" \ | ||||||||||||||||||||||
| python3 "${TEST_ROOT}/helpers/scripts/git_http_backend_server.py" 0 & | ||||||||||||||||||||||
| SERVER_PID=$! | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -52,7 +54,7 @@ git init | |||||||||||||||||||||
| # Ensure cleanup on exit | ||||||||||||||||||||||
| cleanup() { | ||||||||||||||||||||||
| kill "$SERVER_PID" 2>/dev/null || true | ||||||||||||||||||||||
| rm -f "$PORT_FILE" "$READY_FILE" "$INFO_FILE" "$UPLOAD_PACK_COUNT_FILE" | ||||||||||||||||||||||
| rm -f "$PORT_FILE" "$READY_FILE" "$INFO_FILE" "$UPLOAD_PACK_COUNT_FILE" "$REQUEST_LOG" | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| trap cleanup EXIT | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -67,6 +69,11 @@ includes = [ | |||||||||||||||||||||
| ] | ||||||||||||||||||||||
| EOF | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Safe-mode parsing must reject an untrusted remote include before cloning it. | ||||||||||||||||||||||
| : >"$REQUEST_LOG" | ||||||||||||||||||||||
| assert_fail "env -u CI -u GITHUB_ACTION -u GITHUB_ACTIONS MISE_SAFE=1 MISE_TRUSTED_CONFIG_PATHS=$TMPDIR/not-trusted MISE_YES=0 mise tasks ls" "fetching remote task include git::" | ||||||||||||||||||||||
| assert_fail "test -s '$REQUEST_LOG'" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # The xtasks/lint directory contains multiple task files | ||||||||||||||||||||||
| # We should be able to see and run them | ||||||||||||||||||||||
| assert_contains "mise tasks" "remote-task" | ||||||||||||||||||||||
|
|
@@ -164,6 +171,18 @@ assert_contains "mise tasks" "toml_table_task" | |||||||||||||||||||||
| assert_contains "mise run toml_task" "toml_task executed" | ||||||||||||||||||||||
| assert_contains "mise run toml_table_task" "toml_table_task executed" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # A remote TOML include may declare another remote file task. Preserve the | ||||||||||||||||||||||
| # local config as trust provenance through both remote layers. | ||||||||||||||||||||||
| NESTED_ARTIFACT_TMPDIR="$TMPDIR/nested-remote-git-artifacts" | ||||||||||||||||||||||
| mkdir -p "$NESTED_ARTIFACT_TMPDIR" | ||||||||||||||||||||||
| assert_contains "TMPDIR=$NESTED_ARTIFACT_TMPDIR MISE_TASK_REMOTE_NO_CACHE=true mise run nested_remote" "nested remote task executed" | ||||||||||||||||||||||
| assert_directory_empty "$NESTED_ARTIFACT_TMPDIR" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| PROVENANCE_STATE_DIR="$TMPDIR/remote-git-provenance-state" | ||||||||||||||||||||||
| mkdir -p "$PROVENANCE_STATE_DIR" | ||||||||||||||||||||||
| assert_contains "MISE_STATE_DIR=$PROVENANCE_STATE_DIR MISE_TRUSTED_CONFIG_PATHS='' MISE_YES=1 mise run nested_remote" "nested remote task executed" | ||||||||||||||||||||||
| assert "find '$PROVENANCE_STATE_DIR/trusted-configs' -mindepth 1 \\( -type f -o -type l \\) 2>/dev/null | wc -l | tr -d ' '" "1" | ||||||||||||||||||||||
|
Comment on lines
+181
to
+184
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Verify the trusted source, not only the entry count. Line 184 only checks that one trust-state entry exists. A regression that trusts the remote TOML file instead of the defining local After the successful run, untrust Proposed test addition assert_contains "MISE_STATE_DIR=$PROVENANCE_STATE_DIR MISE_TRUSTED_CONFIG_PATHS='' MISE_YES=1 mise run nested_remote" "nested remote task executed"
assert "find '$PROVENANCE_STATE_DIR/trusted-configs' -mindepth 1 \\( -type f -o -type l \\) 2>/dev/null | wc -l | tr -d ' '" "1"
+assert_succeed "MISE_STATE_DIR=$PROVENANCE_STATE_DIR MISE_TRUSTED_CONFIG_PATHS='' MISE_YES=0 mise untrust mise.toml"
+assert_fail "MISE_STATE_DIR=$PROVENANCE_STATE_DIR MISE_TRUSTED_CONFIG_PATHS='' MISE_YES=0 MISE_TASK_REMOTE_NO_CACHE=true mise run nested_remote" "not trusted"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| mise cache clear # Clear cache to force redownload | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ################################################################################# | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| #!/usr/bin/env bash | ||
|
|
||
| # Remote task discovery must trust the defining config before it fetches or | ||
| # renders metadata, including through explicitly included task files. | ||
|
|
||
| HTTP_PORT_FILE="$TMPDIR/mise_http_test_port" | ||
| MARKER="$TMPDIR/remote-template-executed" | ||
| REQUEST_MARKER="$TMPDIR/remote-template-requested" | ||
| MISE_HTTP_TEST_PORT_FILE="$HTTP_PORT_FILE" \ | ||
| MISE_HTTP_REQUEST_MARKER="$REQUEST_MARKER" \ | ||
| python3 "${TEST_ROOT}/helpers/scripts/http_test_server.py" 0 & | ||
| HTTP_SERVER_PID=$! | ||
|
|
||
| cleanup() { | ||
| kill "$HTTP_SERVER_PID" 2>/dev/null || true | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| wait_for_file "$HTTP_PORT_FILE" "HTTP test server port file" 30 "$HTTP_SERVER_PID" | ||
| HTTP_PORT=$(cat "$HTTP_PORT_FILE") | ||
|
|
||
| UNTRUSTED_ENV="env -u CI -u GITHUB_ACTION -u GITHUB_ACTIONS MISE_PARANOID=1 MISE_TRUSTED_CONFIG_PATHS=$TMPDIR/not-trusted MISE_YES=0 MISE_REMOTE_TEMPLATE_MARKER=$MARKER" | ||
|
|
||
| cat <<EOF >mise.toml | ||
| [tasks.remote_template] | ||
| file = "http://localhost:${HTTP_PORT}/test/remote-template" | ||
|
|
||
| [tasks.remote_dep] | ||
| run = "echo remote dependency" | ||
| EOF | ||
|
|
||
| # Both gates report which one refused, so the assertions below can tell a | ||
| # blocked fetch apart from a blocked render. | ||
| REMOTE_URL="http://localhost:${HTTP_PORT}/test/remote-template" | ||
| FETCH_REFUSED="fetching remote task $REMOTE_URL requires its defining config to be trusted" | ||
| RENDER_REFUSED="remote task metadata from $REMOTE_URL requires its defining config to be trusted" | ||
|
|
||
| # Safe mode is a code-execution boundary, not a network one: an untrusted | ||
| # config can already download arbitrary URLs there through the HTTP-based | ||
| # backends, so the fetch is allowed. Rendering the fetched header is what must | ||
| # still require trust. | ||
| assert_fail "$UNTRUSTED_ENV MISE_SAFE=1 mise tasks ls" "$RENDER_REFUSED" | ||
| [[ -e $REQUEST_MARKER ]] || fail "safe mode should still fetch the remote task" | ||
| # belt and braces: in safe mode the tera exec stub would also stop this | ||
| [[ ! -e $MARKER ]] || fail "safe-mode remote metadata template rendered without trust" | ||
| rm -f "$REQUEST_MARKER" | ||
|
|
||
| # Outside paranoid mode a plain-tasks config body parses trust-exempt, so these | ||
| # failures come from the fetch gate rather than a parse-time trust check. The | ||
| # safe-mode run above populated the remote-task cache, so force no-cache to | ||
| # keep "no request was made" meaningful. | ||
| NON_PARANOID_UNTRUSTED_ENV="env -u CI -u GITHUB_ACTION -u GITHUB_ACTIONS MISE_TRUSTED_CONFIG_PATHS=$TMPDIR/not-trusted MISE_YES=0 MISE_TASK_REMOTE_NO_CACHE=true MISE_REMOTE_TEMPLATE_MARKER=$MARKER" | ||
| assert_fail "$NON_PARANOID_UNTRUSTED_ENV mise tasks ls" "$FETCH_REFUSED" | ||
| assert_fail "$NON_PARANOID_UNTRUSTED_ENV mise tasks info remote_template" "$FETCH_REFUSED" | ||
| assert_fail "$NON_PARANOID_UNTRUSTED_ENV mise tasks validate remote_template" "$FETCH_REFUSED" | ||
| assert_fail "$NON_PARANOID_UNTRUSTED_ENV mise tasks deps remote_template" "$FETCH_REFUSED" | ||
| # Cache inspection is passive discovery too and must honor the same gate. | ||
| assert_fail "$NON_PARANOID_UNTRUSTED_ENV mise cache task remote_template" "$FETCH_REFUSED" | ||
| [[ ! -e $REQUEST_MARKER ]] || fail "remote task was fetched despite the untrusted fetch gate" | ||
| [[ ! -e $MARKER ]] || fail "remote metadata template executed despite the untrusted fetch gate" | ||
|
|
||
| # Passive discovery authorizes, it must not acquire: even under MISE_YES=1 a | ||
| # listing must not persist trust for the whole config root. | ||
| YES_STATE_DIR="$TMPDIR/yes-trust-state" | ||
| mkdir -p "$YES_STATE_DIR" | ||
| assert_fail "env -u CI -u GITHUB_ACTION -u GITHUB_ACTIONS MISE_TRUSTED_CONFIG_PATHS=$TMPDIR/not-trusted MISE_YES=1 MISE_STATE_DIR=$YES_STATE_DIR MISE_TASK_REMOTE_NO_CACHE=true mise tasks ls" "$FETCH_REFUSED" | ||
| assert "find '$YES_STATE_DIR/trusted-configs' -mindepth 1 2>/dev/null | wc -l | tr -d ' '" "0" | ||
|
|
||
| # watch expands task dependencies (fetching remote tasks) before any run is | ||
| # spawned, so safe mode must refuse it before discovery starts. | ||
| assert_fail "$UNTRUSTED_ENV MISE_SAFE=1 MISE_TASK_REMOTE_NO_CACHE=true mise watch remote_template" "disabled in safe mode" | ||
| [[ ! -e $REQUEST_MARKER ]] || fail "safe-mode watch fetched an untrusted remote task" | ||
|
|
||
| # no-cache so this re-fetches rather than reusing the safe-mode download above | ||
| assert_succeed "$UNTRUSTED_ENV mise trust mise.toml" | ||
| assert_succeed "$UNTRUSTED_ENV MISE_TASK_REMOTE_NO_CACHE=true mise tasks ls" | ||
| [[ -e $REQUEST_MARKER ]] || fail "trusted passive discovery did not fetch the remote task" | ||
| [[ -e $MARKER ]] || fail "trusted remote metadata template did not render" | ||
|
|
||
| # Trust gets the header past the render gate, but safe mode still refuses the | ||
| # code execution inside it, exactly as it does for a local task template. | ||
| assert_fail "$UNTRUSTED_ENV MISE_SAFE=1 mise tasks ls" "exec() is disabled in safe mode" | ||
| assert_succeed "$UNTRUSTED_ENV mise untrust mise.toml" | ||
|
|
||
| # A template-free remote task is inert once fetched, so safe mode can list it | ||
| # without trust. | ||
| PLAIN_DIR="$TMPDIR/plain-remote" | ||
| mkdir -p "$PLAIN_DIR" | ||
| cat <<EOF >"$PLAIN_DIR/mise.toml" | ||
| [tasks.remote_plain] | ||
| file = "http://localhost:${HTTP_PORT}/test/mytask" | ||
| EOF | ||
| assert_contains "cd '$PLAIN_DIR' && $UNTRUSTED_ENV MISE_SAFE=1 mise tasks ls" "remote_plain" | ||
|
|
||
| # A trusted config vouches for an explicitly included task file even when that | ||
| # file is outside the config's trust root. Preserve that provenance when the | ||
| # include declares a remote task, including for temporary no-cache snapshots. | ||
| INCLUDED_TASK_DIR="$TMPDIR/included-remote-tasks" | ||
| mkdir -p "$INCLUDED_TASK_DIR" | ||
| cat <<EOF >"$INCLUDED_TASK_DIR/tasks.toml" | ||
| [included_remote_template] | ||
| file = "http://localhost:${HTTP_PORT}/test/remote-template" | ||
| EOF | ||
| cat <<EOF >mise.toml | ||
| [task_config] | ||
| includes = ["$INCLUDED_TASK_DIR/tasks.toml"] | ||
| EOF | ||
|
|
||
| assert_succeed "$UNTRUSTED_ENV mise trust mise.toml" | ||
| assert_contains "$UNTRUSTED_ENV mise tasks info included_remote_template" "remote_dep" | ||
| assert_contains "$UNTRUSTED_ENV MISE_TASK_REMOTE_NO_CACHE=true mise tasks info included_remote_template" "remote_dep" | ||
| assert_succeed "$UNTRUSTED_ENV mise untrust mise.toml" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -158,6 +158,7 @@ impl TasksLs { | |
| // always pass no_cache=false as the command doesn't take no-cache argument | ||
| // MISE_TASK_REMOTE_NO_CACHE env var is still respected if set | ||
| TaskFetcher::new(false) | ||
| .require_trust_before_fetch() | ||
| .fetch_tasks(&config, &mut tasks) | ||
| .await?; | ||
|
Comment on lines
+161
to
163
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Apply visibility filtering after remote metadata resolution. A remote 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.