Skip to content

Security hardening: input validation, symlink protection, and output sanitization#3

Open
assisted-by-ai wants to merge 1 commit intoKicksecure:masterfrom
assisted-by-ai:claude/security-audit-9eTrJ
Open

Security hardening: input validation, symlink protection, and output sanitization#3
assisted-by-ai wants to merge 1 commit intoKicksecure:masterfrom
assisted-by-ai:claude/security-audit-9eTrJ

Conversation

@assisted-by-ai
Copy link
Copy Markdown
Contributor

Summary

This PR implements multiple security hardening measures across the systemcheck suite, focusing on preventing information disclosure, symlink attacks, and injection vulnerabilities.

Key Changes

File Write Security (canary-download)

  • Replace unsafe open() calls with os.open() using O_NOFOLLOW | O_CREAT | O_TRUNC flags to prevent symlink attacks
  • Set restrictive file permissions (0o600) at creation time for sensitive files
  • Use context managers (with os.fdopen()) for proper file handle management

Exception Handling & Information Disclosure Prevention

  • Sanitize exception messages to only print exception type names instead of full messages that may leak network/proxy details
  • Replace verbose error logging with high-level status messages in signify-openbsd verification
  • Remove detailed stdout/stderr output that could expose system paths or sensitive information

Input Validation & Injection Prevention

  • Add html_escape() function to prevent HTML/output injection in displayed messages
  • Apply HTML escaping to all user-controlled output in error messages and logs
  • Validate date formats (YYYY-MM-DD) before passing to date command in check_debian_eol.bsh
  • Validate bind mount paths to only allow safe characters (alphanumeric, /, -, _, .)
  • Validate tor_consensus_status before interpolating into command arguments

Configuration File Security (preparation.bsh, log-checker)

  • Add permission checks to refuse sourcing world-writable config files
  • Warn users when config files with unsafe permissions are encountered

Process Timeout Protection (log-checker)

  • Wrap grep commands with timeout --kill-after="5" "60" to prevent hanging on large log files

Function Validation (function_manual_run.bsh)

  • Add check to verify function exists before attempting execution
  • Prevent arbitrary command execution through undefined function names

Code Cleanup

  • Remove unnecessary chmod 700 call since mktemp --directory already creates directories with mode 0700

Notable Implementation Details

  • All file operations now use low-level os.open() with explicit flags for atomic, safe creation
  • HTML escaping is applied consistently across all output that may contain user-controlled data
  • Validation patterns use regex matching for date formats and path characters
  • Timeout protection prevents denial-of-service through resource exhaustion in log processing

https://claude.ai/code/session_016QyDhcj8jetxrGKewnf5nn

- Prevent arbitrary command execution via --function by validating against
  declared bash functions (function_manual_run.bsh)
- Use safe JSON parsing with error handling instead of direct key access
  that leaks tracebacks (check_tor_socks_or_trans_port.bsh)
- Secure file writes with O_NOFOLLOW|O_CREAT|0o600 to prevent symlink
  attacks and ensure restrictive permissions (canary-download)
- Validate date strings match YYYY-MM-DD before passing to date command
  (check_debian_eol.bsh)
- Add 60s timeout to grep operations on log files (log-checker)
- Validate tor_consensus_status before interpolating into leaprun args
  (check_anondate.bsh)
- Sanitize error messages to avoid leaking network/system details
  (canary-download)
- Validate findmnt output characters before processing (check_qubes.bsh)
- Refuse to source world-writable config files (preparation.bsh, log-checker)
- HTML-escape external data embedded in HTML output messages
- Comment out redundant chmod after mktemp --directory

https://claude.ai/code/session_016QyDhcj8jetxrGKewnf5nn
Copy link
Copy Markdown
Contributor

@ArrayBolt3 ArrayBolt3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integrated the useful parts of this in ArrayBolt3@0ccf6d1. Comments below.

Comment on lines -76 to +78
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the system is infected, malware can likely already see these network/proxy details. If the system isn't infected, there's no point in hiding this, other than perhaps to prevent users from sharing sensitive information from logs when pasting logs, and users should be careful about that in any event. Probably not worth changing this.

Comment on lines -101 to +108
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")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary. This script runs as user canary, so it won't be able to write outside of directories that it is allowed to by the filesystem (and beyond that it's also AppArmor-confined, so it can't "accidentally" write outside of places it's supposed to). Using with is nice, but this is a very simple, short block of code, so I don't think it's needed here.

Comment on lines -136 to +141
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rejected-ish because this is another attempt at preventing logging of "too much" data, and we don't need to harden against that. However, this doesn't actually print the full backtrace, which is probably useful, so I'll port this to using the traceback module.

Comment on lines -145 to +154
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rejected, the paths involved here not only are safe to log, but are public information, as they are embedded in this file.

Comment on lines -176 to +186
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rejected for the same reason as the other file-open hardening part.

Comment on lines -111 to +119
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" \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rejected for the same reason as above.

Comment on lines -133 to +142
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" | \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rejected for the same reason as above.

Comment on lines +15 to +25
## Escape HTML special characters to prevent output injection.
html_escape() {
local str="${1:-}"
str="${str//&/&}"
str="${str//</&lt;}"
str="${str//>/&gt;}"
str="${str//\"/&quot;}"
str="${str//\'/&#39;}"
printf '%s' "$str"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rejected, we already have sanitize-string in helper-scripts, which does a better job than this.

Comment on lines -23 to +43
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly accepted, with the same caveats as with the above source_config modification.

Comment on lines -337 to +355
## 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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is false AFAICT, the string umask is not present anywhere in systemcheck's code and I don't see a umask call in any of Kicksecure's packages that plausibly sets the umask to 077 when running systemcheck normally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants