Is it safe to store a VM's command output into a dom0 variable? (Sanitizing variables in dom0)

I have a question regarding the security of storing a VM’s output into a dom0 variable.

I wrote a shell script in dom0 to automate my backup process. It interacts with a backup VM (sys-backup) and a USB VM (sys-usb) with a YubiKey. To make the automation work, the script captures the command output from the VM (like encryption keys or backup targets) and stores it into a dom0 variable, which is then passed as an argument to subsequent dom0 backup commands (like qvm-backup).

However, I am feeling a bit anxious about this approach. Conceptually, storing a VM’s output into a dom0 variable feels almost identical to copy-pasting untrusted data directly into dom0. If the VM were to be compromised, this could potentially open the door to a command injection attack or shell exploitation in dom0.

My questions are:

  1. Is this variable assignment pattern inherently dangerous in Qubes OS?
  2. Since I absolutely need these variables to run dom0-specific backup commands, is there any way to make this process safer?
  3. How can I properly sanitize or validate the VM’s output within dom0 before storing it as a variable to prevent any malicious shell execution?

Here is the script

#!/bin/bash
qvm-shutdown --all --wait
qvm-start sys-backup sys-usb

sleep 30

YUBIKEY=$(qvm-usb l | grep “Yubikey” | awk ‘{print $1}’)
USBS=$(qvm-block l | grep “Storage” | awk ‘{print $1}’)

BACKUP_VMS=$(qvm-ls --raw-list | grep -E “^(vault|anon-whonix)”)
BACKUP_PW=“changeme”
echo $BACKUP_PW | qvm-backup --dest-vm sys-backup --passphrase-file - -y /home/user/ $BACKUP_VMS

SYS_BACKUP_PATH=$(qvm-run -p sys-backup “echo qubes-backup-*”)
USB_BACKUP_PATH=/mnt/$SYS_BACKUP_PATH

qvm-usb attach sys-backup $YUBIKEY

for i in ${USBS[@]}; do
qvm-block attach sys-backup $i

qvm-run -p sys-backup '
	YK_CHALLENGE="changeme"
	LUKS_PW=$(ykman otp calculate 2 "$(echo -n "$YK_CHALLENGE" | xxd -p)" |tr -d "\n" )
	printf "%s" "$LUKS_PW" | sudo cryptsetup luksOpen /dev/xvdi backup_usb --key-file -
	sudo mount /dev/mapper/backup_usb /mnt
	BACKUP_PATH=$(echo /home/user/qubes-backup-*)
	cp "$BACKUP_PATH" /mnt/
	sudo sync'

USB_NAME=$(qvm-block l | grep $i | awk '{print $6,$7,$8}')

if echo $BACKUP_PW | qvm-backup-restore --dest-vm sys-backup --verify-only --passphrase-file - $USB_BACKUP_PATH; then
	notify-send "Backup $USB_NAME: SUCCESS & VERIFIED" -u critical -i dialog-information
else
	notify-send "Backup $USB_NAME: VERIFICATION FAILED" -u critical -i error
fi

qvm-run -p sys-backup '
	sudo umount /mnt
	sudo cryptsetup luksClose backup_usb'

qvm-block detach sys-backup $i

done

qvm-shutdown --all --wait
qvm-start sys-usb sys-whonix

Check your script with shellcheck. You will see lots of things to fix.

General rule: do not use variable names in caps as you can override environment variables by mistake.

As for the actual question: it is safe if the command output is safe, i.e. take care of proper error handling and output validation.

foo="$(qvm-run -p ...)" is not inherently dangerous. (Assumption: no ancient, exploitable pipe → buffer bug in bash.)

You could write a simple qrexec API with strict constraints on arguments and input/output (if you write the API that way) to do a safer, checked equivalent of qvm-run. You could set up /etc/qubes/policy.d permissions to offload some of the work to an intermediary “backup management” VM, for less direct involvement of dom0.

After doing foo="$(qvm-run -p ...)", "$foo" is just a string. Always keep it quoted. grep bar <<< "$foo" is safe. [[ "$foo" =~ bar ]] is safe, stuff like "${foo/baz/qux}" is safe. Be careful about using it in a context where it could be interpreted as a command flag, e.g. echo "$foo" | ... is a little suss.

2 Likes

Each time I found myself in that situation, there was a way to avoid the variable issue. Here you can put your backup in a specific directory or make sys-backup move the file?

2 Likes

Reading arbitrary bytes from a lesser trusted context into a Bash variable in a higher trusted one (especially dom0) does already feel a bit iffy to me, even before considering how to use that variable later.

I don’t have a specific Bash vulnerability in mind, but here’s some paranoid thinking for example: Bash has the language construct ${#var}, which returns the length of the contents of var. That length is not about bytes but characters, so Bash must do character set related parsing - dealing with malformed Unicode and other horrors. Does it do all that only when the ${#var} construct is actually used, or is the length immediately precomputed upon reading into var? I hope it’s the former, but this seems like the kind of implementation detail that could be changed at any point for performance reasons. (Bash releases tend to be pretty casual about much more significant changes.)

To make reading untrusted data into a Bash variable safer, I use qvm-run --filter-escape-chars (or for VM-to-VM calls: qrexec-client-vm -t) to restrict the output to a subset of ASCII. If an even narrower set of characters is sufficient, I might pipe that output into tr, e.g. to throw away everything except numbers and lowercase letters: qvm-run --filter-escape-chars ... | LC_ALL=C tr -dc 0-9a-z. Maybe also append e.g. | head -c 1M to prevent the VM from sending gigabytes of data and exhausting all memory.

Exactly, every piece of data you avoid passing across a VM boundary is one that you don’t have to worry about sanitizing.

It’s possible to pass a command instead of a filename to qvm-backup and qvm-backup-restore. You could use that to generate the backup’s filename in dom0, then you don’t have to query it from sys-backup at all:

backup_file=/home/user/qubes-backup-$(date +'%Y-%m-%dT%H%M%S')
qvm-backup [...] "dd bs=128K iflag=fullblock conv=fsync of=$backup_file"

Those date and dd commands are copied based on what /etc/qubes-rpc/qubes.Backup does by default (if a filename is passed, not a command).

4 Likes