diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ee923724..bc0c3ddf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,12 +29,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 #v4.35.1 with: languages: 'python' - name: Autobuild - uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 + uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 #v4.35.1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 #v4.32.3 + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 #v4.35.1 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1e044ec9..bb646a3e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: issues: write steps: - - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d #v10.1.1 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f #v10.2.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 diff --git a/.github/workflows/stale_pr.yml b/.github/workflows/stale_pr.yml index 6dfcbe99..44605aab 100644 --- a/.github/workflows/stale_pr.yml +++ b/.github/workflows/stale_pr.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d #v10.1.1 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f #v10.2.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} # Do not automatically mark PR/issue as stale diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6482e242..80cc92dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,13 +43,13 @@ jobs: ignore_words_file: .codespellignore - name: Get editorconfig-checker - uses: editorconfig-checker/action-editorconfig-checker@4b6cd6190d435e7e084fb35e36a096e98506f7b9 #v2.1.0 + uses: editorconfig-checker/action-editorconfig-checker@840e866d93b8e032123c23bac69dece044d4d84c #v2.2.0 - name: Run editorconfig-checker run: editorconfig-checker - name: Check python code formatting with black - uses: psf/black@6305bf1ae645ab7541be4f5028a86239316178eb #26.1.0 + uses: psf/black@c6755bb741b6481d6b3d3bb563c83fa060db96c9 #26.3.1 with: src: "./test" options: "--check --diff --color" diff --git a/README.md b/README.md index 622ff202..7ec57000 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ If you'd rather not donate (_which is okay!_), there are other ways you can help - [Patreon](https://patreon.com/pihole) - [Hetzner Cloud](https://hetzner.cloud/?ref=7aceisRX3AzA) _affiliate link_ - [Digital Ocean](https://www.digitalocean.com/?refcode=344d234950e1) _affiliate link_ -- [Stickermule](https://www.stickermule.com/unlock?ref_id=9127301701&utm_medium=link&utm_source=invite) _earn a $10 credit after your first purchase_ +- [Stickermule](https://www.stickermule.com/unlock?ref_id=6055890701&utm_medium=link&utm_source=invite) _earn a $10 credit after your first purchase_ - [Amazon US](https://www.amazon.com/exec/obidos/redirect-home/pihole09-20) _affiliate link_ - Spreading the word about our software and how you have benefited from it diff --git a/advanced/Scripts/piholeCheckout.sh b/advanced/Scripts/piholeCheckout.sh index a6df46f2..bd6facc4 100755 --- a/advanced/Scripts/piholeCheckout.sh +++ b/advanced/Scripts/piholeCheckout.sh @@ -186,7 +186,9 @@ checkout() { echo -e " ${INFO} Checking for ${COL_YELLOW}${binary}${COL_NC} binary on https://ftl.pi-hole.net" - if check_download_exists "$path"; then + local download_status + check_download_exists "$path" && download_status=0 || download_status=$? + if [ $download_status -eq 0 ]; then echo " ${TICK} Binary exists" echo "${2}" > /etc/pihole/ftlbranch chmod 644 /etc/pihole/ftlbranch @@ -210,15 +212,13 @@ checkout() { # Update local and remote versions via updatechecker /opt/pihole/updatecheck.sh else - local status - status=$? - if [ $status -eq 1 ]; then + if [ $download_status -eq 1 ]; then # Binary for requested branch is not available, may still be # int he process of being built or CI build job failed printf " %b Binary for requested branch is not available, please try again later.\\n" "${CROSS}" printf " If the issue persists, please contact Pi-hole Support and ask them to re-generate the binary.\\n" exit 1 - elif [ $status -eq 2 ]; then + elif [ $download_status -eq 2 ]; then printf " %b Unable to download from ftl.pi-hole.net. Please check your Internet connection and try again later.\\n" "${CROSS}" exit 1 else diff --git a/advanced/Scripts/piholeDebug.sh b/advanced/Scripts/piholeDebug.sh index 964fff9b..1ca5ae3c 100755 --- a/advanced/Scripts/piholeDebug.sh +++ b/advanced/Scripts/piholeDebug.sh @@ -41,8 +41,10 @@ else #OVER="\r\033[K" fi -# shellcheck source=/dev/null -. /etc/pihole/versions +# shellcheck source=./advanced/Scripts/utils.sh +source /opt/pihole/utils.sh + +loadVersionFile /etc/pihole/versions # Read the value of an FTL config key. The value is printed to stdout. get_ftl_conf_value() { @@ -169,7 +171,7 @@ initialize_debug() { # Display that the debug process is beginning log_write "${COL_PURPLE}*** [ INITIALIZING ]${COL_NC}" # Timestamp the start of the log - log_write "${INFO} $(date "+%Y-%m-%d:%H:%M:%S") debug log has been initialized." + log_write "${INFO} $(date "+%Y-%m-%d %H:%M:%S") debug log has been initialized." # Uptime of the system # credits to https://stackoverflow.com/questions/28353409/bash-format-uptime-to-show-days-hours-minutes system_uptime=$(uptime | awk -F'( |,|:)+' '{if ($7=="min") m=$6; else {if ($7~/^day/){if ($9=="min") {d=$6;m=$8} else {d=$6;h=$8;m=$9}} else {h=$6;m=$7}}} {print d+0,"days,",h+0,"hours,",m+0,"minutes"}') diff --git a/advanced/Scripts/piholeNetworkFlush.sh b/advanced/Scripts/piholeNetworkFlush.sh index a8721476..a156362d 100755 --- a/advanced/Scripts/piholeNetworkFlush.sh +++ b/advanced/Scripts/piholeNetworkFlush.sh @@ -15,7 +15,7 @@ if [[ -f ${coltable} ]]; then source ${coltable} fi -readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" +PI_HOLE_SCRIPT_DIR="/opt/pihole" utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" # shellcheck source=./advanced/Scripts/utils.sh source "${utilsfile}" diff --git a/advanced/Scripts/utils.sh b/advanced/Scripts/utils.sh index d000a6db..8ce88c2c 100755 --- a/advanced/Scripts/utils.sh +++ b/advanced/Scripts/utils.sh @@ -42,6 +42,57 @@ addOrEditKeyValPair() { fi } +####################### +# Safely loads key=value pairs from the Pi-hole versions cache file. +# Unlike `source`, this function never executes file content as shell code. +# Only known keys are assigned, and values are validated against a strict +# character allowlist to prevent shell injection. +# +# Takes one argument: path to the versions file +# Returns 0 in all cases (compatible with set -e) +# Example loadVersionFile "/etc/pihole/versions" +####################### +loadVersionFile() { + local file="${1}" + local line key value + + [ -f "${file}" ] || return 0 + + while IFS= read -r line || [ -n "${line}" ]; do + # Skip blank lines and comments + case "${line}" in + ''|\#*) continue ;; + esac + + # Require KEY=VALUE format (key must be non-empty) + key="${line%%=*}" + value="${line#*=}" + [ -z "${key}" ] && continue + [ "${key}" = "${line}" ] && continue # no '=' found + + # Allowlist: only assign known version-file keys + case "${key}" in + CORE_VERSION|CORE_BRANCH|CORE_HASH|\ + GITHUB_CORE_VERSION|GITHUB_CORE_HASH|\ + WEB_VERSION|WEB_BRANCH|WEB_HASH|\ + GITHUB_WEB_VERSION|GITHUB_WEB_HASH|\ + FTL_VERSION|FTL_BRANCH|FTL_HASH|\ + GITHUB_FTL_VERSION|GITHUB_FTL_HASH|\ + DOCKER_VERSION|GITHUB_DOCKER_VERSION) ;; + *) continue ;; + esac + + # Validate value: allow only characters safe in version strings and branch names. + # Permits: letters, digits, dot, hyphen, underscore, slash, plus sign, and empty string. + case "${value}" in + *[!a-zA-Z0-9._/+\-]*) continue ;; + esac + + # Safe to assign: key is from the allowlist, value contains no shell metacharacters + eval "${key}=\${value}" + done < "${file}" +} + ####################### # returns FTL's PID based on the content of the pihole-FTL.pid file # diff --git a/advanced/Scripts/version.sh b/advanced/Scripts/version.sh index e932fe63..95f0f583 100755 --- a/advanced/Scripts/version.sh +++ b/advanced/Scripts/version.sh @@ -8,17 +8,18 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. -# Source the versions file populated by updatechecker.sh +# shellcheck source=./advanced/Scripts/utils.sh +. /opt/pihole/utils.sh + +# Load the versions file populated by updatechecker.sh cachedVersions="/etc/pihole/versions" -if [ -f ${cachedVersions} ]; then - # shellcheck source=/dev/null - . "$cachedVersions" +if [ -f "${cachedVersions}" ]; then + loadVersionFile "${cachedVersions}" else echo "Could not find /etc/pihole/versions. Running update now." pihole updatechecker - # shellcheck source=/dev/null - . "$cachedVersions" + loadVersionFile "${cachedVersions}" fi main() { diff --git a/automated install/basic-install.sh b/automated install/basic-install.sh index 9a8a87ad..bc045b4e 100755 --- a/automated install/basic-install.sh +++ b/automated install/basic-install.sh @@ -2042,8 +2042,8 @@ FTLcheckUpdate() { # Check whether or not the binary for this FTL branch actually exists. If not, then there is no update! local status - if ! check_download_exists "${path}"; then - status=$? + check_download_exists "${path}" && status=0 || status=$? + if [ "${status}" -ne 0 ]; then if [ "${status}" -eq 1 ]; then printf " %b Branch \"%s\" is not available.\\n" "${INFO}" "${ftlBranch}" printf " %b Use %bpihole checkout ftl [branchname]%b to switch to a valid branch.\\n" "${INFO}" "${COL_GREEN}" "${COL_NC}" diff --git a/gravity.sh b/gravity.sh index 22e6c742..0c5a65f4 100755 --- a/gravity.sh +++ b/gravity.sh @@ -86,16 +86,17 @@ generate_gravity_database() { # Build gravity tree gravity_build_tree() { + local table="$1" local str - str="Building tree" + str="Building ${table} tree" echo -ne " ${INFO} ${str}..." # The index is intentionally not UNIQUE as poor quality adlists may contain domains more than once - output=$({ pihole-FTL sqlite3 -ni "${gravityTEMPfile}" "CREATE INDEX idx_gravity ON gravity (domain, adlist_id);"; } 2>&1) + output=$({ pihole-FTL sqlite3 -ni "${gravityTEMPfile}" "CREATE INDEX idx_${table} ON ${table} (domain, adlist_id);"; } 2>&1) status="$?" if [[ "${status}" -ne 0 ]]; then - echo -e "\\n ${CROSS} Unable to build gravity tree in ${gravityTEMPfile}\\n ${output}" + echo -e "\\n ${CROSS} Unable to build ${table} tree in ${gravityTEMPfile}\\n ${output}" echo -e " ${INFO} If you have a large amount of domains, make sure your Pi-hole has enough RAM available\\n" return 1 fi @@ -844,11 +845,11 @@ gravity_Table_Count() { local str="${2}" local num num="$(pihole-FTL sqlite3 -ni "${gravityTEMPfile}" "SELECT COUNT(*) FROM ${table};")" - if [[ "${table}" == "gravity" ]]; then + if [[ "${table}" == "gravity" || "${table}" == "antigravity" ]]; then local unique unique="$(pihole-FTL sqlite3 -ni "${gravityTEMPfile}" "SELECT COUNT(*) FROM (SELECT DISTINCT domain FROM ${table});")" echo -e " ${INFO} Number of ${str}: ${num} (${COL_BOLD}${unique} unique domains${COL_NC})" - pihole-FTL sqlite3 -ni "${gravityTEMPfile}" "INSERT OR REPLACE INTO info (property,value) VALUES ('gravity_count',${unique});" + pihole-FTL sqlite3 -ni "${gravityTEMPfile}" "INSERT OR REPLACE INTO info (property,value) VALUES ('${table}_count',${unique});" else echo -e " ${INFO} Number of ${str}: ${num}" fi @@ -858,11 +859,14 @@ gravity_Table_Count() { gravity_ShowCount() { # Here we use the table "gravity" instead of the view "vw_gravity" for speed. # It's safe to replace it here, because right after a gravity run both will show the exactly same number of domains. + echo "" gravity_Table_Count "gravity" "gravity domains" + gravity_Table_Count "antigravity" "antigravity domains" gravity_Table_Count "domainlist WHERE type = 1 AND enabled = 1" "exact denied domains" gravity_Table_Count "domainlist WHERE type = 3 AND enabled = 1" "regex denied filters" gravity_Table_Count "domainlist WHERE type = 0 AND enabled = 1" "exact allowed domains" gravity_Table_Count "domainlist WHERE type = 2 AND enabled = 1" "regex allowed filters" + echo "" } # Trap Ctrl-C @@ -1149,7 +1153,8 @@ update_gravity_timestamp fix_owner_permissions "${gravityTEMPfile}" # Build the tree -timeit gravity_build_tree +timeit gravity_build_tree gravity +timeit gravity_build_tree antigravity # Compute numbers to be displayed (do this after building the tree to get the # numbers quickly from the tree instead of having to scan the whole database) diff --git a/pihole b/pihole index 5af46fa6..306c9deb 100755 --- a/pihole +++ b/pihole @@ -30,15 +30,10 @@ readonly apifile="${PI_HOLE_SCRIPT_DIR}/api.sh" source "${apifile}" versionsfile="/etc/pihole/versions" -if [ -f "${versionsfile}" ]; then - # Only source versionsfile if the file exits - # fixes a warning during installation where versionsfile does not exist yet - # but gravity calls `pihole -status` and thereby sourcing the file - # shellcheck source=/dev/null - source "${versionsfile}" -fi +# Load version variables without executing file content as shell code. +# loadVersionFile() only assigns known keys with validated values. +loadVersionFile "${versionsfile}" -# TODO: We can probably remove the reliance on this function too, just tell people to pihole-FTL --config webserver.api.password "password" SetWebPassword() { if [ -n "$2" ] ; then readonly PASSWORD="$2" diff --git a/test/requirements.txt b/test/requirements.txt index 61f18c4e..dbeef1f0 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -2,5 +2,5 @@ pyyaml == 6.0.3 pytest == 9.0.2 pytest-xdist == 3.8.0 pytest-testinfra == 10.2.2 -tox == 4.35.0 +tox == 4.51.0 pytest-clarity == 1.0.1