diff --git a/usr/libexec/systemcheck/canary-download b/usr/libexec/systemcheck/canary-download index 01700d9..4cc2615 100755 --- a/usr/libexec/systemcheck/canary-download +++ b/usr/libexec/systemcheck/canary-download @@ -73,7 +73,9 @@ def main(): #data = requests.get(url, proxies=proxies, auth=(username, password)) except Exception as e: - print('connect error: {}'.format(e) , file=sys.stderr) + ## Sanitize: only print exception type, not full message which may + ## leak network/proxy details. + print('connect error: {}'.format(type(e).__name__), file=sys.stderr) sys.exit(5) data_content_length = len(data.content) @@ -98,10 +100,12 @@ def main(): signify_openbsd_public_key = "/usr/share/keyrings/derivative.pub" timeout_seconds = 5 - file_object = open(canary_txt_embed_sig, "w") - file_object.write(url_body) - #file_object.write("extraneous content test") - file_object.close() + ## Use O_CREAT|O_WRONLY|O_TRUNC|O_NOFOLLOW to prevent symlink attacks + ## and ensure restrictive permissions (0o600) from creation. + fd = os.open(canary_txt_embed_sig, os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, "w") as file_object: + file_object.write(url_body) + #file_object.write("extraneous content test") if not data.status_code == 200: msg = "invalid data.status_code: " + str(data.status_code) @@ -133,8 +137,8 @@ def main(): process.kill() sys.exit(6) except BaseException: - error_message = str(sys.exc_info()[0]) - msg = "canary signify-openbsd unknown error. sys.exc_info: " + error_message + error_type = type(sys.exc_info()[1]).__name__ + msg = "canary signify-openbsd unknown error: " + error_type print(msg, file=sys.stderr) process.kill() sys.exit(7) @@ -142,8 +146,12 @@ def main(): stdout, stderr = process.communicate(timeout=2) stdout = stdout.decode("UTF-8").strip() stderr = stderr.decode("UTF-8").strip() - print("stdout:", str(stdout), file=sys.stderr) - print("stderr:", str(stderr), file=sys.stderr) + ## Only log signify-openbsd output at a high level; avoid leaking + ## file paths or system details in error output. + if process.returncode != 0: + print("signify-openbsd: verification failed (exit code {})".format(process.returncode), file=sys.stderr) + else: + print("signify-openbsd: verification succeeded", file=sys.stderr) if process.returncode != 0: msg = "Could not verify canary using signify-openbsd." @@ -173,9 +181,9 @@ def main(): unixtime_str = str(unixtime_digit) print("unixtime: " + unixtime_str) - file_object = open(canary_unixtime, "w") - file_object.write(unixtime_str) - file_object.close() + fd = os.open(canary_unixtime, os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, "w") as file_object: + file_object.write(unixtime_str) if __name__ == "__main__": diff --git a/usr/libexec/systemcheck/check_anondate.bsh b/usr/libexec/systemcheck/check_anondate.bsh index 0f45f1b..b7d3768 100755 --- a/usr/libexec/systemcheck/check_anondate.bsh +++ b/usr/libexec/systemcheck/check_anondate.bsh @@ -36,6 +36,11 @@ check_anondate_do() { fi if [ ! "$tor_consensus_status" = "none" ]; then + ## Validate tor_consensus_status before interpolating into leaprun arguments. + case "$tor_consensus_status" in + verified|unverified) true ;; + *) echo "ERROR: Unexpected tor_consensus_status value: '$tor_consensus_status'" >&2 ; return 1 ;; + esac if leaprun anondate-${tor_consensus_status}-only-current-time-in-valid-range ; then current_time_in_valid_range=true else diff --git a/usr/libexec/systemcheck/check_debian_eol.bsh b/usr/libexec/systemcheck/check_debian_eol.bsh index 3a9c9ba..70c3c93 100755 --- a/usr/libexec/systemcheck/check_debian_eol.bsh +++ b/usr/libexec/systemcheck/check_debian_eol.bsh @@ -52,6 +52,10 @@ See also: $link_cli" ## ELTS has very limited security support and is therefore ignored. ## Zero-indexed field 5 = EOL, 6 = LTS EOL os_eol_date="${os_info_bit_list[5]}" + if ! [[ "${os_eol_date}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + warn_unknown_eol_date + return 0 + fi os_eol_timestamp="$(date --date="${os_eol_date}" '+%s')" if [ -z "${os_eol_timestamp}" ]; then warn_unknown_eol_date @@ -59,7 +63,12 @@ See also: $link_cli" fi os_lts_eol_date="${os_info_bit_list[6]}" - os_lts_eol_timestamp="$(date --date="${os_lts_eol_date}" '+%s')" + if ! [[ "${os_lts_eol_date}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + true "warn: os_lts_eol_date does not match expected YYYY-MM-DD format" + os_lts_eol_timestamp="${os_eol_timestamp}" + else + os_lts_eol_timestamp="$(date --date="${os_lts_eol_date}" '+%s')" + fi if [ -z "${os_lts_eol_timestamp}" ]; then true "warn: could not determine LTS EOL timestamp" os_lts_eol_timestamp="${os_eol_timestamp}" diff --git a/usr/libexec/systemcheck/check_qubes.bsh b/usr/libexec/systemcheck/check_qubes.bsh index b49f5d8..c05bcbc 100755 --- a/usr/libexec/systemcheck/check_qubes.bsh +++ b/usr/libexec/systemcheck/check_qubes.bsh @@ -23,7 +23,7 @@ check_qubes_network_interface() {

Reading qubes-db failed for some reason. This could be due to a broken upgrade, race condition or other bug. Try restarting the VM to see if this error persists.



qubesdb-read /qubes-ip output: -

$qubes_ip +

$(html_escape "$qubes_ip")



Check the systemd unit status of qubes-db.

1. Open a terminal. ($start_menu_instructions_system_first_part Terminal) @@ -161,6 +161,11 @@ check_qubes_persistent_mounts() { -- "${should_be_ephemeral_path}" \ | sed 's/.*\[\([^]]*\)\].*/\1/' || true) for bind_mount_item in "${bind_mount_list[@]}"; do + ## Validate: only allow safe path characters (alphanumeric, /, -, _, .). + if ! [[ "${bind_mount_item}" =~ ^[a-zA-Z0-9/_.-]+$ ]]; then + printf '%s\n' "WARNING: Skipping unexpected findmnt output: '${bind_mount_item}'" >&2 + continue + fi printf '%s\n' "${bind_mount_item}" >&2 if [ -d "/rw/${bind_mount_item}" ]; then MSG="\ @@ -333,9 +338,9 @@ This is expected because not running in Template.

" qubes_update_proxy_test_result_torified="false" local MSG="

Qubes UpdatesProxy Reachability Result: Ok, non-torified update proxy reachable. -curl_status_message:

$curl_status_message
-PROXY_META:
${PROXY_META}
-curl_output:
$curl_output

" +curl_status_message:
$(html_escape "$curl_status_message")
+PROXY_META:
$(html_escape "${PROXY_META}")
+curl_output:
$(html_escape "$curl_output")

" $output_x ${output_opts[@]} --messagex --typex "warning" --message "$MSG" $output_cli ${output_opts[@]} --messagecli --typecli "warning" --message "$MSG" } diff --git a/usr/libexec/systemcheck/check_tor_socks_or_trans_port.bsh b/usr/libexec/systemcheck/check_tor_socks_or_trans_port.bsh index f554f7f..87cce67 100755 --- a/usr/libexec/systemcheck/check_tor_socks_or_trans_port.bsh +++ b/usr/libexec/systemcheck/check_tor_socks_or_trans_port.bsh @@ -182,12 +182,21 @@ check_tor_socks_or_trans_port() { if [ "$verbose" -ge "2" ]; then local MSG="\

$FUNCNAME $1: check_tor_out_file_content_sanitized: -

$check_tor_out_file_content_sanitized

" +
$(html_escape "$check_tor_out_file_content_sanitized")

" $output_x ${output_opts[@]} --messagex --typex "info" --message "$MSG" $output_cli ${output_opts[@]} --messagecli --typecli "info" --message "$MSG" fi - tor_detected_raw="$(printf "%s" "$check_tor_out_file_content_sanitized" | python3 -c "import sys, json; print(json.load(sys.stdin)['IsTor'])")" || { tor_detected_raw="$?" ; true; }; + tor_detected_raw="$(printf "%s" "$check_tor_out_file_content_sanitized" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) +except (json.JSONDecodeError, ValueError): + print('JSONError', end='') + sys.exit(1) +value = data.get('IsTor', '') +print(value, end='') +")" || { tor_detected_raw="$?" ; true; }; ## example tor_detected_raw: # True ## example tor_detected_raw: @@ -214,7 +223,15 @@ check_tor_socks_or_trans_port() { local json_ip_exit_code ip_raw json_ip_exit_code="0" - ip_raw="$(printf "%s" "$check_tor_out_file_content_sanitized" | python3 -c "import sys, json; print(json.load(sys.stdin)['IP'])")" || { json_ip_exit_code="$?" ; true; }; + ip_raw="$(printf "%s" "$check_tor_out_file_content_sanitized" | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) +except (json.JSONDecodeError, ValueError): + sys.exit(1) +value = data.get('IP', '') +print(value, end='') +")" || { json_ip_exit_code="$?" ; true; }; ## example ip_raw: ## 94.242.204.74 diff --git a/usr/libexec/systemcheck/function_manual_run.bsh b/usr/libexec/systemcheck/function_manual_run.bsh index bd47a0e..22ef314 100755 --- a/usr/libexec/systemcheck/function_manual_run.bsh +++ b/usr/libexec/systemcheck/function_manual_run.bsh @@ -14,6 +14,12 @@ function_manual_run() { if [ "$FUNCTION" = "" ]; then return 0 fi + if ! declare -F "$FUNCTION" >/dev/null 2>&1; then + echo "$SCRIPTNAME ERROR: function '$FUNCTION' is not a known bash function. Refusing to execute." >&2 + EXIT_CODE="1" + cleanup + return 0 + fi $FUNCTION cleanup return 0 diff --git a/usr/libexec/systemcheck/log-checker b/usr/libexec/systemcheck/log-checker index 753f836..3e58a48 100755 --- a/usr/libexec/systemcheck/log-checker +++ b/usr/libexec/systemcheck/log-checker @@ -24,11 +24,16 @@ source /usr/libexec/helper-scripts/strings.bsh ## Copied from /usr/libexec/systemcheck/preparation.bsh. source_config() { shopt -s nullglob - local i + local i file_perms for i in \ /etc/systemcheck.d/*.conf \ /usr/local/etc/systemcheck.d/*.conf \ ; do + ## Refuse to source world-writable config files. + file_perms="$(stat -c '%a' "$i" 2>/dev/null)" || continue + case "$file_perms" in + *[2367]) echo "WARNING: Skipping world-writable config file: $i" >&2 ; continue ;; + esac bash -n "$i" source "$i" done @@ -66,7 +71,8 @@ check_service_logs() { journal_ignore_patterns_list+=( "virtualbox" ) fi - grep --extended-regexp --ignore-case "$journal_search_pattern_list" -- "$TMPDIR/journalctl_output.txt" \ + timeout --kill-after="5" "60" \ + grep --extended-regexp --ignore-case "$journal_search_pattern_list" -- "$TMPDIR/journalctl_output.txt" \ | tee -- "$TMPDIR/journalctl_matched.txt" >/dev/null safe-rm --force -- "$TMPDIR/journalctl_output.txt" @@ -101,14 +107,16 @@ check_service_logs() { # #printf '%s\n' "$counter: '$journal_ignore_pattern_item'" >/dev/null # done - "${grep_ignore_fixed_items_command[@]}" -- "$TMPDIR/journalctl_matched.txt" \ + timeout --kill-after="5" "60" \ + "${grep_ignore_fixed_items_command[@]}" -- "$TMPDIR/journalctl_matched.txt" \ | tee -- "$TMPDIR/journalctl_fixed_filtered.txt" >/dev/null safe-rm --force -- "$TMPDIR/journalctl_matched.txt" patterns="$(printf '%s|' "${journal_ignore_patterns_list[@]}" | head -c-1)" - grep --invert-match --extended-regexp --ignore-case "$patterns" -- "$TMPDIR/journalctl_fixed_filtered.txt" \ + timeout --kill-after="5" "60" \ + grep --invert-match --extended-regexp --ignore-case "$patterns" -- "$TMPDIR/journalctl_fixed_filtered.txt" \ | tee -- "$TMPDIR/journalctl_match_filtered.txt" >/dev/null ## Creates file: '$TMPDIR/journalctl_match_filtered.txt_br' @@ -130,7 +138,8 @@ check_critical_logs() { ## The space before ' BUG:' is important to avoid matching 'debug:'. critical_pattern='Bad RAM detected|self-detected stall on CPU| BUG:|nouveau .* channel .* killed!' - grep --ignore-case --extended-regexp "$critical_pattern" -- "$TMPDIR/journalctl_match_filtered.txt_br" | \ + timeout --kill-after="5" "60" \ + grep --ignore-case --extended-regexp "$critical_pattern" -- "$TMPDIR/journalctl_match_filtered.txt_br" | \ stcatn safe-rm --force -- "$TMPDIR/journalctl_match_filtered.txt_br" diff --git a/usr/libexec/systemcheck/preparation.bsh b/usr/libexec/systemcheck/preparation.bsh index 95da16e..e589c9a 100755 --- a/usr/libexec/systemcheck/preparation.bsh +++ b/usr/libexec/systemcheck/preparation.bsh @@ -12,6 +12,17 @@ systemcheck_autostart_functions+=" msgdispatcher_init " systemcheck_autostart_functions+=" sysmaint_state_detection " systemcheck_autostart_functions+=" cleanup " +## Escape HTML special characters to prevent output injection. +html_escape() { + local str="${1:-}" + str="${str//&/&}" + str="${str///>}" + str="${str//\"/"}" + str="${str//\'/'}" + printf '%s' "$str" +} + output_if_verbose() { if [ "$verbose" -ge "1" ]; then "$@" @@ -20,11 +31,16 @@ output_if_verbose() { source_config() { shopt -s nullglob - local i + local i file_perms for i in \ /etc/systemcheck.d/*.conf \ /usr/local/etc/systemcheck.d/*.conf \ ; do + ## Refuse to source world-writable config files. + file_perms="$(stat -c '%a' "$i" 2>/dev/null)" || continue + case "$file_perms" in + *[2367]) echo "WARNING: Skipping world-writable config file: $i" >&2 ; continue ;; + esac bash -n "$i" source "$i" done @@ -334,8 +350,9 @@ preparation() { ## returns: derivative_deb_package_version get_local_derivative_version - ## Prepare temporary directory. - chmod 700 "$TEMP_DIR" + ## mktemp --directory already creates the directory with mode 0700 (via + ## umask), so an explicit chmod is unnecessary. + #chmod 700 "$TEMP_DIR" if [ -f "/usr/share/anon-gw-base-files/gateway" ]; then VM="Whonix-Gateway"