V9.14.022.2026.06.11: add path security helpers
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
#!/bin/bash
|
||||
# SPDX-Version: 3.0
|
||||
# SPDX-CreationInfo: 2026-06-11; WEIDNER, Marc S.; <msw@coresecret.dev>
|
||||
# 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.; <msw@coresecret.dev>
|
||||
# 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
|
||||
Reference in New Issue
Block a user