From 74897d85b159bc840794a6af9c94259d478fdf1744fd3db2ab7bc5ff93d2e43d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20H=2E=20Zimnol?= Date: Thu, 11 Jun 2026 05:07:33 +0200 Subject: [PATCH] V9.14.022.2026.06.11: add path security helpers --- lib/lib_build_directory.sh | 340 +++++++++++++++++++++++++++++++ lib/lib_debug_sanitizer.sh | 92 +++++++++ lib/lib_secret_validation.sh | 384 +++++++++++++++++++++++++++++++++++ var/global.var.sh | 3 + 4 files changed, 819 insertions(+) create mode 100644 lib/lib_build_directory.sh create mode 100644 lib/lib_debug_sanitizer.sh create mode 100644 lib/lib_secret_validation.sh diff --git a/lib/lib_build_directory.sh b/lib/lib_build_directory.sh new file mode 100644 index 0000000..9e30971 --- /dev/null +++ b/lib/lib_build_directory.sh @@ -0,0 +1,340 @@ +#!/bin/bash +# SPDX-Version: 3.0 +# SPDX-CreationInfo: 2026-06-11; WEIDNER, Marc S.; +# SPDX-ExternalRef: GIT https://git.coresecret.dev/msw/CISS.debian.live.builder.git +# SPDX-FileContributor: WEIDNER, Marc S.; Centurion Intelligence Consulting Agency +# SPDX-FileCopyrightText: 2024-2026; WEIDNER, Marc S.; +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: LicenseRef-CNCL-1.1 OR LicenseRef-CCLA-1.1 +# SPDX-LicenseComment: This file is part of the CISS.debian.installer.secure framework. +# SPDX-PackageName: CISS.debian.live.builder +# SPDX-Security-Contact: security@coresecret.eu + +guard_sourcing || return "${ERR_GUARD_SRCE}" + +####################################### +# Prints a generic build-directory validation error without disclosing a path. +# Arguments: +# 1: unsafe input class +# 2: quiet flag +# Returns: +# ERR_BUILD_PATH +####################################### +build_directory_validation_error() { + declare error_class="$1" quiet="${2:-false}" + + if [[ "${quiet}" != "true" ]]; then + printf "\e[91m❌ Unsafe build-directory input rejected: %s. \e[0m\n" "${error_class}" >&2 + fi + + return "${ERR_BUILD_PATH:-217}" +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f build_directory_validation_error + +####################################### +# Canonicalises an existing path or a path whose parent exists. +# Arguments: +# 1: candidate path +# 2: output variable name +# Returns: +# 0: on success +# ERR_BUILD_PATH: on failure +####################################### +canonicalize_build_directory() { + declare candidate="$1" output_variable="$2" basename="" parent="" resolved_path="" + + [[ -n "${candidate}" && "${candidate}" == /* ]] || return "${ERR_BUILD_PATH:-217}" + [[ "${candidate}" == "/" ]] || candidate="${candidate%/}" + [[ ! -L "${candidate}" ]] || return "${ERR_BUILD_PATH:-217}" + + if [[ -e "${candidate}" ]]; then + resolved_path="$(realpath "${candidate}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" + else + basename="${candidate##*/}" + parent="${candidate%/*}" + [[ -n "${parent}" ]] || parent="/" + [[ -n "${basename}" && "${basename}" != "." && "${basename}" != ".." && -d "${parent}" ]] || return "${ERR_BUILD_PATH:-217}" + resolved_path="$(realpath "${parent}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" + resolved_path="${resolved_path%/}/${basename}" + fi + + [[ "${candidate}" == "${resolved_path}" ]] || return "${ERR_BUILD_PATH:-217}" + printf -v "${output_variable}" '%s' "${resolved_path}" + + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f canonicalize_build_directory + +####################################### +# Validates a build directory against the repository cleanup policy. +# Globals: +# VAR_TMP_SECRET +# VAR_WORKDIR +# Arguments: +# 1: candidate path +# 2: output variable name +# 3: quiet flag +# Returns: +# 0: on success +# ERR_BUILD_PATH: on failure +####################################### +validate_build_directory_path() { + declare candidate="$1" output_variable="${2:-}" quiet="${3:-false}" secret_root="" validated_path="" workdir="" + declare -a rejected_paths=( + "/" "/bin" "/boot" "/dev" "/etc" "/home" "/lib" "/lib64" "/media" "/mnt" "/opt" "/proc" "/root" "/run" "/sbin" "/srv" + "/sys" "/tmp" "/usr" "/usr/local" "/var" "/var/lib" "/var/tmp" + ) + declare rejected_path="" + + canonicalize_build_directory "${candidate}" validated_path || { + build_directory_validation_error \ + "path is empty, non-absolute, non-canonical, missing its parent, or is a symlink" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + } + + for rejected_path in "${rejected_paths[@]}"; do + if [[ "${validated_path}" == "${rejected_path}" ]]; then + build_directory_validation_error "broad or system parent directory" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + fi + done + + workdir="$(realpath "${VAR_WORKDIR}" 2>/dev/null)" || { + build_directory_validation_error "repository work directory cannot be resolved" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + } + secret_root="$(realpath "${VAR_TMP_SECRET}" 2>/dev/null)" || { + build_directory_validation_error "secret root cannot be resolved" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + } + + if [[ "${workdir}" == "${validated_path}" || "${workdir}" == "${validated_path}/"* || "${validated_path}" == "${workdir}/"* \ + || "${validated_path}" == "${secret_root}" || "${validated_path}" == "${secret_root}/"* ]]; then + build_directory_validation_error "path is outside the dedicated build-directory policy" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + fi + + if [[ -n "${output_variable}" ]]; then + printf -v "${output_variable}" '%s' "${validated_path}" + fi + + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f validate_build_directory_path + +####################################### +# Validates the builder-owned marker for an exact build directory. +# Globals: +# EUID +# Arguments: +# 1: candidate path +# 2: quiet flag +# Returns: +# 0: on success +# ERR_BUILD_PATH: on failure +####################################### +validate_build_directory_marker() { + declare candidate="$1" quiet="${2:-false}" directory_mode="" directory_owner="" marker="" marker_build_dir="" + declare expected_marker_value="" marker_link_count="" marker_value="" mode="" owner="" + + validate_build_directory_path "${candidate}" marker_build_dir "${quiet}" || return "${ERR_BUILD_PATH:-217}" + [[ -d "${marker_build_dir}" ]] || { + build_directory_validation_error "build directory does not exist" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + } + + directory_owner="$(secure_stat -c '%u' "${marker_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" + directory_mode="$(secure_stat -c '%a' "${marker_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" + if [[ "${directory_owner}" != "${EUID}" ]] || (( (8#${directory_mode} & 022) != 0 )); then + build_directory_validation_error "build directory ownership or permissions are unsafe" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + fi + + marker="${marker_build_dir}/.ciss-live-builder-owned" + if [[ -L "${marker}" || ! -f "${marker}" ]]; then + build_directory_validation_error "builder-owned marker is missing or unsafe" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + fi + + owner="$(secure_stat -c '%u' "${marker}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" + mode="$(secure_stat -c '%a' "${marker}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" + marker_link_count="$(secure_stat -c '%h' "${marker}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" + marker_value="$(cat "${marker}" || exit $?; printf '.')" || return "${ERR_BUILD_PATH:-217}" + marker_value="${marker_value%.}" + expected_marker_value="${marker_build_dir}"$'\n' + + if [[ "${owner}" != "${EUID}" || "${mode}" != "400" || "${marker_link_count}" != "1" \ + || "${marker_value}" != "${expected_marker_value}" ]]; then + build_directory_validation_error "builder-owned marker does not match the exact directory" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + fi + + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f validate_build_directory_marker + +####################################### +# Validates an existing exact subpath below a marker-owned build directory. +# Arguments: +# 1: build directory +# 2: relative subpath +# 3: output variable name +# 4: quiet flag +# Returns: +# 0: on success +# ERR_BUILD_PATH: on failure +####################################### +validate_build_directory_subpath() { + declare build_directory="$1" relative_path="$2" output_variable="$3" quiet="${4:-false}" + declare candidate_subpath="" resolved_subpath="" + + validate_build_directory_marker "${build_directory}" "${quiet}" || return "${ERR_BUILD_PATH:-217}" + + if [[ -z "${relative_path}" || "${relative_path}" == /* || "${relative_path}" == "." || "${relative_path}" == ".." \ + || "${relative_path}" == ../* || "${relative_path}" == */../* || "${relative_path}" == */.. \ + || "${relative_path}" == ./* || "${relative_path}" == */./* || "${relative_path}" == */. ]]; then + build_directory_validation_error "unsafe relative cleanup subpath" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + fi + + candidate_subpath="${build_directory}/${relative_path}" + if [[ -L "${candidate_subpath}" || ! -e "${candidate_subpath}" ]]; then + build_directory_validation_error "cleanup subpath is missing or is a symlink" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + fi + + resolved_subpath="$(realpath "${candidate_subpath}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" + if [[ "${resolved_subpath}" != "${candidate_subpath}" || "${resolved_subpath}" != "${build_directory}/"* ]]; then + build_directory_validation_error "cleanup subpath escapes the exact build directory" "${quiet}" + return "${ERR_BUILD_PATH:-217}" + fi + + printf -v "${output_variable}" '%s' "${resolved_subpath}" + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f validate_build_directory_subpath + +####################################### +# Initialises a new or empty build directory and its ownership marker. +# Arguments: +# 1: candidate path +# 2: output variable name +# Returns: +# 0: on success +# ERR_BUILD_PATH: on failure +####################################### +initialize_build_directory() { + declare candidate="$1" output_variable="$2" directory_mode="" directory_owner="" existing_entry="" + declare initialized_build_dir="" marker="" + + validate_build_directory_path "${candidate}" initialized_build_dir || return "${ERR_BUILD_PATH:-217}" + + if [[ ! -e "${initialized_build_dir}" ]]; then + mkdir -m 0700 "${initialized_build_dir}" || return "${ERR_BUILD_PATH:-217}" + fi + + validate_build_directory_path "${initialized_build_dir}" initialized_build_dir || return "${ERR_BUILD_PATH:-217}" + [[ -d "${initialized_build_dir}" && ! -L "${initialized_build_dir}" ]] || return "${ERR_BUILD_PATH:-217}" + + directory_owner="$(secure_stat -c '%u' "${initialized_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" + directory_mode="$(secure_stat -c '%a' "${initialized_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}" + if [[ "${directory_owner}" != "${EUID}" ]] || (( (8#${directory_mode} & 022) != 0 )); then + build_directory_validation_error "build directory ownership or permissions are unsafe" + return "${ERR_BUILD_PATH:-217}" + fi + + marker="${initialized_build_dir}/.ciss-live-builder-owned" + + if [[ -e "${marker}" || -L "${marker}" ]]; then + validate_build_directory_marker "${initialized_build_dir}" || return "${ERR_BUILD_PATH:-217}" + else + existing_entry="$(find "${initialized_build_dir}" -mindepth 1 -maxdepth 1 -print -quit)" || return "${ERR_BUILD_PATH:-217}" + if [[ -n "${existing_entry}" ]]; then + build_directory_validation_error "non-empty directory has no builder-owned marker" + return "${ERR_BUILD_PATH:-217}" + fi + (umask 077; printf '%s\n' "${initialized_build_dir}" >| "${marker}") || return "${ERR_BUILD_PATH:-217}" + chmod 0400 "${marker}" || return "${ERR_BUILD_PATH:-217}" + validate_build_directory_marker "${initialized_build_dir}" || return "${ERR_BUILD_PATH:-217}" + fi + + printf -v "${output_variable}" '%s' "${initialized_build_dir}" + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f initialize_build_directory + +####################################### +# Removes paths with a one-filesystem boundary where supported. +# Arguments: +# paths to remove +# Returns: +# rm exit status +####################################### +remove_build_paths() { + # shellcheck disable=SC2312 + if rm --help 2>&1 | grep -q -- '--one-file-system'; then + rm -rf --one-file-system -- "$@" + else + rm -rf -- "$@" + fi +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f remove_build_paths + +####################################### +# Deletes all content except the ownership marker from an exact build directory. +# Arguments: +# 1: candidate path +# Returns: +# 0: on success +# ERR_BUILD_PATH: on failure +####################################### +clean_build_directory_contents() { + declare candidate="$1" build_entry="" + declare -a build_entries=() + declare -i old_dotglob=0 old_failglob=0 old_nullglob=0 + + validate_build_directory_marker "${candidate}" || return "${ERR_BUILD_PATH:-217}" + + shopt -q dotglob && old_dotglob=1 + shopt -q failglob && old_failglob=1 + shopt -q nullglob && old_nullglob=1 + shopt -s dotglob nullglob + shopt -u failglob + + build_entries=("${candidate}"/*) + for build_entry in "${build_entries[@]}"; do + [[ "${build_entry}" == "${candidate}/.ciss-live-builder-owned" ]] && continue + remove_build_paths "${build_entry}" || { + if (( old_dotglob )); then shopt -s dotglob; else shopt -u dotglob; fi + if (( old_failglob )); then shopt -s failglob; else shopt -u failglob; fi + if (( old_nullglob )); then shopt -s nullglob; else shopt -u nullglob; fi + return "${ERR_BUILD_PATH:-217}" + } + done + + if (( old_dotglob )); then shopt -s dotglob; else shopt -u dotglob; fi + if (( old_failglob )); then shopt -s failglob; else shopt -u failglob; fi + if (( old_nullglob )); then shopt -s nullglob; else shopt -u nullglob; fi + + validate_build_directory_marker "${candidate}" || return "${ERR_BUILD_PATH:-217}" + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f clean_build_directory_contents + +# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh diff --git a/lib/lib_debug_sanitizer.sh b/lib/lib_debug_sanitizer.sh new file mode 100644 index 0000000..76a415c --- /dev/null +++ b/lib/lib_debug_sanitizer.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# SPDX-Version: 3.0 +# SPDX-CreationInfo: 2026-06-11; WEIDNER, Marc S.; +# SPDX-ExternalRef: GIT https://git.coresecret.dev/msw/CISS.debian.live.builder.git +# SPDX-FileContributor: WEIDNER, Marc S.; Centurion Intelligence Consulting Agency +# SPDX-FileCopyrightText: 2024-2026; WEIDNER, Marc S.; +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: LicenseRef-CNCL-1.1 OR LicenseRef-CCLA-1.1 +# SPDX-LicenseComment: This file is part of the CISS.debian.installer.secure framework. +# SPDX-PackageName: CISS.debian.live.builder +# SPDX-Security-Contact: security@coresecret.eu + +guard_sourcing || return "${ERR_GUARD_SRCE}" + +####################################### +# Replaces exact registered secret values in one controlled log file. +# Globals: +# _ARY_SECRET_REDACTION_VALUES +# Arguments: +# 1: log file +# Returns: +# 0: on success or missing log +# ERR_SANITIZING: on failure +####################################### +sanitize_debug_log() { + declare log_file="$1" log_text="" replacement="" secret_value="" tmp_file="" + + [[ -n "${log_file}" && -f "${log_file}" ]] || return 0 + [[ ! -L "${log_file}" ]] || return "${ERR_SANITIZING:-133}" + + log_text="$(cat "${log_file}" || exit $?; printf '.')" || return "${ERR_SANITIZING:-133}" + log_text="${log_text%.}" + + for secret_value in "${_ARY_SECRET_REDACTION_VALUES[@]}"; do + [[ -n "${secret_value}" ]] || continue + printf -v replacement '%*s' "${#secret_value}" '' + replacement="${replacement// /*}" + log_text="${log_text//"${secret_value}"/"${replacement}"}" + done + + tmp_file="$(mktemp "${log_file}.sanitize.XXXXXX")" || return "${ERR_SANITIZING:-133}" + chmod 0600 "${tmp_file}" || { + rm -f "${tmp_file}" + return "${ERR_SANITIZING:-133}" + } + printf '%s' "${log_text}" >| "${tmp_file}" || { + rm -f "${tmp_file}" + return "${ERR_SANITIZING:-133}" + } + mv -f "${tmp_file}" "${log_file}" || { + rm -f "${tmp_file}" + return "${ERR_SANITIZING:-133}" + } + + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f sanitize_debug_log + +####################################### +# Runs the final exact-value sanitisation pass for controlled logs. +# Globals: +# LOG_DEBUG +# LOG_ERROR +# LOG_VAR +# Arguments: +# None +# Returns: +# 0: on success +# ERR_SANITIZING: on failure +####################################### +sanitize_debug_logs() { + declare log_file="" + declare -a log_files=("${LOG_DEBUG:-}" "${LOG_VAR:-}" "${LOG_ERROR:-}") + + set +x + if [[ -e "/proc/$$/fd/42" || -e "/dev/fd/42" ]]; then + exec 42>&- + fi + + for log_file in "${log_files[@]}"; do + sanitize_debug_log "${log_file}" || return "${ERR_SANITIZING:-133}" + done + + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f sanitize_debug_logs + +# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh diff --git a/lib/lib_secret_validation.sh b/lib/lib_secret_validation.sh new file mode 100644 index 0000000..4b4ff9d --- /dev/null +++ b/lib/lib_secret_validation.sh @@ -0,0 +1,384 @@ +#!/bin/bash +# SPDX-Version: 3.0 +# SPDX-CreationInfo: 2026-06-11; WEIDNER, Marc S.; +# SPDX-ExternalRef: GIT https://git.coresecret.dev/msw/CISS.debian.live.builder.git +# SPDX-FileContributor: WEIDNER, Marc S.; Centurion Intelligence Consulting Agency +# SPDX-FileCopyrightText: 2024-2026; WEIDNER, Marc S.; +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: LicenseRef-CNCL-1.1 OR LicenseRef-CCLA-1.1 +# SPDX-LicenseComment: This file is part of the CISS.debian.installer.secure framework. +# SPDX-PackageName: CISS.debian.live.builder +# SPDX-Security-Contact: security@coresecret.eu + +guard_sourcing || return "${ERR_GUARD_SRCE}" + +if ! declare -p _ARY_SECRET_REDACTION_VALUES >/dev/null 2>&1; then + declare -ga _ARY_SECRET_REDACTION_VALUES=() +fi + +####################################### +# Runs GNU stat on Debian and gstat on macOS development hosts. +# Arguments: +# stat arguments +# Returns: +# stat exit status +####################################### +secure_stat() { + if command -v gstat >/dev/null 2>&1; then + gstat "$@" + else + stat "$@" + fi +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f secure_stat + +####################################### +# Prints a generic secret validation error without disclosing a path or value. +# Arguments: +# 1: unsafe input class +# 2: quiet flag +# Returns: +# ERR_SECRET_PATH +####################################### +secret_validation_error() { + declare error_class="$1" quiet="${2:-false}" + + if [[ "${quiet}" != "true" ]]; then + printf "\e[91m❌ Unsafe secret input rejected: %s. \e[0m\n" "${error_class}" >&2 + fi + + return "${ERR_SECRET_PATH:-216}" +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f secret_validation_error + +####################################### +# Registers an exact known secret value for final log redaction. +# Globals: +# _ARY_SECRET_REDACTION_VALUES +# Arguments: +# 1: secret value +# Returns: +# 0: on success +####################################### +register_secret_value() { + declare secret_value="$1" registered_value="" was_traced="false" + + [[ $- == *x* ]] && was_traced="true" + set +x + + if [[ -n "${secret_value}" ]]; then + for registered_value in "${_ARY_SECRET_REDACTION_VALUES[@]}"; do + if [[ "${registered_value}" == "${secret_value}" ]]; then + [[ "${was_traced}" == "true" ]] && set -x + return 0 + fi + done + _ARY_SECRET_REDACTION_VALUES+=("${secret_value}") + fi + + [[ "${was_traced}" == "true" ]] && set -x + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f register_secret_value + +####################################### +# Registers exact text values from a controlled secret file. +# Globals: +# _ARY_SECRET_REDACTION_VALUES +# Arguments: +# 1: secret file +# Returns: +# 0: on success +####################################### +register_secret_file_for_redaction() { + declare secret_file="$1" secret_line="" secret_text="" was_traced="false" + + [[ $- == *x* ]] && was_traced="true" + set +x + + secret_text="$(cat "${secret_file}" || exit $?; printf '.')" || { + [[ "${was_traced}" == "true" ]] && set -x + return "${ERR_SANITIZING:-133}" + } + secret_text="${secret_text%.}" + register_secret_value "${secret_text}" + + while IFS= read -r secret_line || [[ -n "${secret_line}" ]]; do + register_secret_value "${secret_line}" + done < "${secret_file}" + + [[ "${was_traced}" == "true" ]] && set -x + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f register_secret_file_for_redaction + +####################################### +# Validates a filename-only secret argument. +# Arguments: +# 1: filename +# 2: input class +# Returns: +# 0: on success +# ERR_SECRET_PATH: on failure +####################################### +validate_secret_filename() { + declare filename="$1" input_class="${2:-filename-only secret argument}" + declare filename_regex='^[A-Za-z0-9._@%+=:,~-]+$' + + if [[ -z "${filename}" || "${filename}" == "." || "${filename}" == ".." || "${filename}" == */* \ + || ! "${filename}" =~ ${filename_regex} ]]; then + secret_validation_error "${input_class}" + return "${ERR_SECRET_PATH:-216}" + fi + + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f validate_secret_filename + +####################################### +# Validates a restrictively permissioned secret directory. +# Arguments: +# 1: directory path +# 2: input class +# 3: require tmpfs +# 4: quiet flag +# Returns: +# 0: on success +# ERR_SECRET_PATH: on failure +####################################### +validate_secret_directory() { + declare directory="$1" input_class="${2:-secret directory}" require_tmpfs="${3:-false}" quiet="${4:-false}" + declare fs_type="" mode="" owner="" + + if [[ -z "${directory}" || -L "${directory}" || ! -d "${directory}" ]]; then + secret_validation_error "${input_class}" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + fi + + owner="$(secure_stat -c '%u' "${directory}" 2>/dev/null)" || { + secret_validation_error "${input_class} ownership" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + } + mode="$(secure_stat -c '%a' "${directory}" 2>/dev/null)" || { + secret_validation_error "${input_class} permissions" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + } + + if [[ "${owner}" != "${EUID}" || "${mode}" != "700" ]]; then + secret_validation_error "${input_class} ownership or permissions" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + fi + + if [[ "${require_tmpfs}" == "true" ]]; then + fs_type="$(secure_stat -f -c '%T' "${directory}" 2>/dev/null)" || { + secret_validation_error "${input_class} filesystem" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + } + if [[ "${fs_type}" != "tmpfs" && "${fs_type}" != "ramfs" ]]; then + secret_validation_error "${input_class} is not tmpfs-backed" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + fi + fi + + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f validate_secret_directory + +####################################### +# Validates and registers a secret file. +# Arguments: +# 1: file path +# 2: input class +# 3: register for redaction +# 4: quiet flag +# Returns: +# 0: on success +# ERR_SECRET_PATH: on failure +####################################### +validate_secret_file() { + declare secret_file="$1" input_class="${2:-secret file}" register_value="${3:-true}" quiet="${4:-false}" + declare link_count="" mode="" owner="" + + if [[ -z "${secret_file}" || -L "${secret_file}" || ! -f "${secret_file}" ]]; then + secret_validation_error "${input_class}" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + fi + + owner="$(secure_stat -c '%u' "${secret_file}" 2>/dev/null)" || { + secret_validation_error "${input_class} ownership" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + } + mode="$(secure_stat -c '%a' "${secret_file}" 2>/dev/null)" || { + secret_validation_error "${input_class} permissions" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + } + link_count="$(secure_stat -c '%h' "${secret_file}" 2>/dev/null)" || { + secret_validation_error "${input_class} link count" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + } + + if [[ "${owner}" != "${EUID}" || "${link_count}" != "1" || ( "${mode}" != "400" && "${mode}" != "600" ) ]]; then + secret_validation_error "${input_class} ownership, permissions, or link count" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + fi + + if [[ "${register_value}" == "true" ]]; then + register_secret_file_for_redaction "${secret_file}" + fi + + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f validate_secret_file + +####################################### +# Validates an explicitly supported absolute secret file path. +# Arguments: +# 1: file path +# 2: input class +# Returns: +# 0: on success +# ERR_SECRET_PATH: on failure +####################################### +validate_secret_absolute_file() { + declare secret_file="$1" input_class="${2:-absolute secret file}" resolved_file="" + + if [[ -z "${secret_file}" || "${secret_file}" != /* ]]; then + secret_validation_error "${input_class} must be an absolute path" + return "${ERR_SECRET_PATH:-216}" + fi + + resolved_file="$(realpath "${secret_file}" 2>/dev/null)" || { + secret_validation_error "${input_class} cannot be resolved" + return "${ERR_SECRET_PATH:-216}" + } + if [[ "${resolved_file}" != "${secret_file}" ]]; then + secret_validation_error "${input_class} must be canonical and must not traverse symlinked parents" + return "${ERR_SECRET_PATH:-216}" + fi + + validate_secret_file "${secret_file}" "${input_class}" || return "${ERR_SECRET_PATH:-216}" + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f validate_secret_absolute_file + +####################################### +# Validates an explicitly supported absolute secret directory path. +# Arguments: +# 1: directory path +# 2: input class +# Returns: +# 0: on success +# ERR_SECRET_PATH: on failure +####################################### +validate_secret_absolute_directory() { + declare directory="$1" input_class="${2:-absolute secret directory}" resolved_directory="" + + if [[ -z "${directory}" || "${directory}" != /* ]]; then + secret_validation_error "${input_class} must be an absolute path" + return "${ERR_SECRET_PATH:-216}" + fi + + resolved_directory="$(realpath "${directory}" 2>/dev/null)" || { + secret_validation_error "${input_class} cannot be resolved" + return "${ERR_SECRET_PATH:-216}" + } + if [[ "${resolved_directory}" != "${directory}" ]]; then + secret_validation_error "${input_class} must be canonical and must not traverse symlinked parents" + return "${ERR_SECRET_PATH:-216}" + fi + + validate_secret_directory "${directory}" "${input_class}" || return "${ERR_SECRET_PATH:-216}" + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f validate_secret_absolute_directory + +####################################### +# Validates a filename-only secret file below the fixed secret root. +# Globals: +# VAR_TMP_SECRET +# Arguments: +# 1: filename +# 2: input class +# 3: register for redaction +# Returns: +# 0: on success +# ERR_SECRET_PATH: on failure +####################################### +validate_secret_file_in_root() { + declare filename="$1" input_class="${2:-secret file}" register_value="${3:-true}" + + validate_secret_directory "${VAR_TMP_SECRET}" "secret root" "true" || return "${ERR_SECRET_PATH:-216}" + validate_secret_filename "${filename}" "${input_class} filename" || return "${ERR_SECRET_PATH:-216}" + validate_secret_file "${VAR_TMP_SECRET}/${filename}" "${input_class}" "${register_value}" || return "${ERR_SECRET_PATH:-216}" + + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f validate_secret_file_in_root + +####################################### +# Validates the fixed tmpfs secret staging area and all entries below it. +# Globals: +# VAR_TMP_SECRET +# Arguments: +# 1: quiet flag +# Returns: +# 0: on success +# ERR_SECRET_PATH: on failure +####################################### +validate_secret_staging_area() { + declare quiet="${1:-false}" secret_entries_file="" secret_entry="" + declare -a secret_entries=() + + validate_secret_directory "${VAR_TMP_SECRET}" "secret root" "true" "${quiet}" || return "${ERR_SECRET_PATH:-216}" + + secret_entries_file="$(mktemp)" || return "${ERR_SECRET_PATH:-216}" + if ! find "${VAR_TMP_SECRET}" -xdev -mindepth 1 -print0 >| "${secret_entries_file}"; then + rm -f "${secret_entries_file}" + secret_validation_error "secret-root enumeration failed" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + fi + mapfile -d '' -t secret_entries < "${secret_entries_file}" + rm -f "${secret_entries_file}" + + for secret_entry in "${secret_entries[@]}"; do + if [[ -L "${secret_entry}" ]]; then + secret_validation_error "symlink below secret root" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + elif [[ -d "${secret_entry}" ]]; then + validate_secret_directory "${secret_entry}" "directory below secret root" "false" "${quiet}" \ + || return "${ERR_SECRET_PATH:-216}" + elif [[ -f "${secret_entry}" ]]; then + validate_secret_file "${secret_entry}" "file below secret root" "true" "${quiet}" || return "${ERR_SECRET_PATH:-216}" + else + secret_validation_error "non-regular entry below secret root" "${quiet}" + return "${ERR_SECRET_PATH:-216}" + fi + done + + return 0 +} +### Prevents accidental 'unset -f'. +# shellcheck disable=SC2034 +readonly -f validate_secret_staging_area + +# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh diff --git a/var/global.var.sh b/var/global.var.sh index 0cd3eb0..dccd63c 100644 --- a/var/global.var.sh +++ b/var/global.var.sh @@ -28,6 +28,7 @@ touch "${LOG_ERROR}" && chmod 0600 "${LOG_ERROR}" declare -g __umask="" declare -g VAR_ARCHITECTURE="" +declare -g VAR_ARG_SANITIZED="" declare -g VAR_DROPBEAR_VERSION="2026.91" declare -g VAR_HANDLER_BUILD_DIR="" declare -g VAR_HANDLER_CDI="false" @@ -91,6 +92,8 @@ declare -gir ERR__SSH__PORT=212 # --ssh-port MUST be an integer between '1' and declare -gir ERR_ARG_MSMTCH=213 # Wrong Number of optional Arguments provided declare -gir ERR_DROPBEAR_V=214 # --dropbear-version MUST match the bundled Dropbear tarball version format declare -gir ERR__SOPS__VER=215 # --sops-version MUST match the upstream SOPS semantic version format +declare -gir ERR_SECRET_PATH=216 # Unsafe secret root, filename, or file. +declare -gir ERR_BUILD_PATH=217 # Unsafe build-directory path or marker. declare -gir ERR_SECRETSSYM=251 # VAR_TMP_SECRET is a symlink. declare -gir ERR_NOTABSPATH=252 # Not an absolute path declare -gir ERR_INVLD_CHAR=253 # Invalid Character