Skip to content

Commit 7b36179

Browse files
authored
Check for unnecessary privilege escalation (#1743)
Resolves tiny-pilot/tinypilot-pro#1214 <s>Blocked by https://github.com/tiny-pilot/tinypilot/pull/1744</s> <s>Blocked by https://github.com/tiny-pilot/tinypilot/pull/1745</s> This PR adds a dev script that checks for possible cases of privilege escalation in tinypilot-writable scripts (i.e., `scripts/`). The script only does a superficial check that root privileges were at least considered by matching on: > This script doesn't require root privileges. Example output of `dev-scripts/check-privilege-guard`: ``` $ ./dev-scripts/check-privilege-guard These files are missing a guard against privilege escalation: scripts/is-ssh-enabled scripts/streaming-mode scripts/update-service scripts/upgrade Please add the following check (or similar) to the above scripts: if [[ "${EUID}" == 0 ]]; then >&2 echo "This script doesn't require root privileges." >&2 echo 'Please re-run as tinypilot:' >&2 echo " runuser tinypilot --command '$0 $*'" exit 1 fi ``` Notes 1. <s>These tinypilot-writable scripts legitimately require root privileges: * `scripts/install-bundle` * `script/upgrade` So they do risk being used for privilege escalation, but they are/should never be executed by privileged scripts on the device. I've also added a superficial check for this too.</s> 2. This PR also fixes the privilege escalation issues that `dev-scripts/check-privilege-guard` as picked up. As a reminder, the fix is a runtime error asking for reduced permissions which is something we'll only encounter when we physically test the device. So as a result, this PR also tries to avoid those runtime errors by running these identified scripts as `tinypilot` where needed: ``` runuser tinypilot --command '/opt/tinypilot/scripts/some-script' ``` <a data-ca-tag href="https://codeapprove.com/pr/tiny-pilot/tinypilot/1743"><img src="https://codeapprove.com/external/github-tag-allbg.png" alt="Review on CodeApprove" /></a>
1 parent 1916fb5 commit 7b36179

File tree

6 files changed

+70
-2
lines changed

6 files changed

+70
-2
lines changed

.circleci/config.yml

+9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ jobs:
2222
- run:
2323
name: Run static analysis on bash scripts
2424
command: ./dev-scripts/check-bash
25+
check_privilege_guard:
26+
docker:
27+
- image: cimg/base:2020.01
28+
steps:
29+
- checkout
30+
- run:
31+
name: Check for unnecessary privilege escalation
32+
command: ./dev-scripts/check-privilege-guard
2533
lint_sql:
2634
docker:
2735
- image: cimg/python:3.9.17
@@ -272,6 +280,7 @@ workflows:
272280
jobs:
273281
- check_whitespace
274282
- check_bash
283+
- check_privilege_guard
275284
- lint_sql
276285
- check_style
277286
- decode_edid

debian-pkg/opt/tinypilot-privileged/scripts/collect-debug-logs

+2-2
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ print_info "Checking if filesystem is read-only..."
109109
print_info "Checking if SSH is enabled..."
110110
{
111111
SSH_STATUS="disabled"
112-
if /opt/tinypilot/scripts/is-ssh-enabled ; then
112+
if runuser tinypilot --command '/opt/tinypilot/scripts/is-ssh-enabled' ; then
113113
SSH_STATUS="enabled"
114114
fi
115115
readonly SSH_STATUS
@@ -166,7 +166,7 @@ print_info "Checking for voltage issues..."
166166
print_info "Checking TinyPilot streaming mode..."
167167
{
168168
echo "Streaming mode"
169-
echo "Selected mode: $(/opt/tinypilot/scripts/streaming-mode)"
169+
echo "Selected mode: $(runuser tinypilot --command '/opt/tinypilot/scripts/streaming-mode')"
170170
printf "Current mode: "
171171
# H264 mode is considered active when the last Janus video log line contains
172172
# "Memsink opened; reading frames".

dev-scripts/check-privilege-guard

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/bash
2+
#
3+
# Check that the TinyPilot scripts contain a guard against privilege escalation.
4+
#
5+
# This script enforces a pattern of ensuring that scripts which are writable by
6+
# the `tinypilot` user don't get executed with unnecessary root privileges.
7+
8+
# Exit on first failing command.
9+
set -e
10+
11+
# Exit on unset variable.
12+
set -u
13+
14+
# Find TinyPilot scripts that don't guard against privilege escalation.
15+
MATCHES="$(grep \
16+
--files-without-match \
17+
--fixed-strings \
18+
--regexp "This script doesn't require root privileges." \
19+
scripts/*; true)"
20+
readonly MATCHES
21+
if [[ -n "${MATCHES}" ]]; then
22+
>&2 echo 'These files are missing a guard against privilege escalation:'
23+
>&2 echo "${MATCHES}"
24+
>&2 echo 'Please add the following check (or similar) to the above scripts:'
25+
>&2 cat <<'EOF'
26+
if [[ "${EUID}" == 0 ]]; then
27+
>&2 echo "This script doesn't require root privileges."
28+
>&2 echo 'Please re-run as tinypilot:'
29+
>&2 echo " runuser tinypilot --command '$0 $*'"
30+
exit 1
31+
fi
32+
EOF
33+
exit 1
34+
fi

scripts/is-ssh-enabled

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ set -u
88
# Exit on first error.
99
set -e
1010

11+
if [[ "${EUID}" == 0 ]]; then
12+
>&2 echo "This script doesn't require root privileges."
13+
>&2 echo 'Please re-run as tinypilot:'
14+
>&2 echo " runuser tinypilot --command '$0 $*'"
15+
exit 1
16+
fi
17+
1118
print_help() {
1219
cat << EOF
1320
Usage: ${0##*/} [-h]

scripts/streaming-mode

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ set -e
88
# Exit on unset variable.
99
set -u
1010

11+
if [[ "${EUID}" == 0 ]]; then
12+
>&2 echo "This script doesn't require root privileges."
13+
>&2 echo 'Please re-run as tinypilot:'
14+
>&2 echo " runuser tinypilot --command '$0 $*'"
15+
exit 1
16+
fi
17+
1118
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
1219
readonly SCRIPT_DIR
1320
cd "${SCRIPT_DIR}/.."

scripts/update-service

+11
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
import argparse
1212
import logging
13+
import os
1314
import subprocess
15+
import sys
1416

1517
# We’re importing the log package first because it needs to overwrite the
1618
# app-wide logger class before any other module loads it.
@@ -61,6 +63,15 @@ def main(_):
6163

6264

6365
if __name__ == '__main__':
66+
# Ensure that the script doesn't have unnecessary privileges.
67+
if os.geteuid() == 0:
68+
print("This script doesn't require root privileges.", file=sys.stderr)
69+
print('Please re-run as tinypilot:', file=sys.stderr)
70+
print(' runuser tinypilot --command',
71+
f"'{' '.join(sys.argv)}'",
72+
file=sys.stderr)
73+
sys.exit(1)
74+
6475
parser = argparse.ArgumentParser(
6576
prog='TinyPilot Update Service',
6677
formatter_class=argparse.ArgumentDefaultsHelpFormatter)

0 commit comments

Comments
 (0)