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 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//\"/"}" + 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"