From 3bbb519ed071c9676d6ec818e73beb68a457c563 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 20:42:36 +0000 Subject: [PATCH] fix(birdnet-go): reject path traversal and create parent dir for sqlite path rewrite Addresses review feedback on the sqlite persistence fix (PR #2812): - validate_safe_path now rejects ".." path segments so a relative output.sqlite.path can't traverse outside /config when rewritten. - The rewrite now creates the destination's parent directory, since SQLite won't create one itself for a path like "db/birdnet.db". Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KHJ4o22cdcgdNtb81tBTPJ --- birdnet-go-dev/CHANGELOG.md | 3 +++ birdnet-go-dev/config.yaml | 2 +- .../rootfs/etc/cont-init.d/01-structure.sh | 18 ++++++++++++++++-- birdnet-go/CHANGELOG.md | 3 +++ birdnet-go/config.yaml | 2 +- .../rootfs/etc/cont-init.d/01-structure.sh | 18 ++++++++++++++++-- 6 files changed, 40 insertions(+), 6 deletions(-) diff --git a/birdnet-go-dev/CHANGELOG.md b/birdnet-go-dev/CHANGELOG.md index 533ef56f81..1ef679b164 100644 --- a/birdnet-go-dev/CHANGELOG.md +++ b/birdnet-go-dev/CHANGELOG.md @@ -1,3 +1,6 @@ +## source-20260705-3 (05-07-2026) +- Harden the `output.sqlite.path` rewrite added for the persistence fix: reject paths containing `..` traversal segments (shared `validate_safe_path` check), and create the destination's parent directory under `/config` when the relative path includes a subdirectory (e.g. `db/birdnet.db`), since SQLite does not create missing parent directories itself. + ## source-20260705-2 (05-07-2026) - Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774) diff --git a/birdnet-go-dev/config.yaml b/birdnet-go-dev/config.yaml index 5e402c3dea..9d9880175a 100644 --- a/birdnet-go-dev/config.yaml +++ b/birdnet-go-dev/config.yaml @@ -127,5 +127,5 @@ slug: birdnet-go-dev udev: true url: https://github.com/alexbelgium/hassio-addons usb: true -version: "source-20260705-2" +version: "source-20260705-3" video: true diff --git a/birdnet-go-dev/rootfs/etc/cont-init.d/01-structure.sh b/birdnet-go-dev/rootfs/etc/cont-init.d/01-structure.sh index c554095a10..4c11de5822 100755 --- a/birdnet-go-dev/rootfs/etc/cont-init.d/01-structure.sh +++ b/birdnet-go-dev/rootfs/etc/cont-init.d/01-structure.sh @@ -17,12 +17,22 @@ normalize_path() { # Reject paths containing characters that would break the SQL/YAML literals # we substitute into below. We deliberately allow only "safe" filename chars. +# Also reject ".." path segments so a relative path can't traverse outside +# the directory we prefix it with (e.g. "/config") when we rewrite it below. validate_safe_path() { local p="$1" if [[ ! "$p" =~ ^[A-Za-z0-9._/-]+$ ]]; then bashio::log.fatal "Refusing unsafe path: '$p' (only [A-Za-z0-9._/-] allowed)" exit 1 fi + local segment + local IFS=/ + for segment in $p; do + if [[ "$segment" == ".." ]]; then + bashio::log.fatal "Refusing unsafe path: '$p' (path traversal '..' is not allowed)" + exit 1 + fi + done } if [ ! -f "$CONFIG_LOCATION" ]; then @@ -149,8 +159,12 @@ bashio::log.info "Seeding default configuration values (only if missing)" CURRENT_SQLITE_PATH="$(yq -r '.output.sqlite.path // ""' "$CONFIG_LOCATION")" if [[ -n "$CURRENT_SQLITE_PATH" && "$CURRENT_SQLITE_PATH" != /* ]]; then validate_safe_path "$CURRENT_SQLITE_PATH" - bashio::log.warning "output.sqlite.path ('$CURRENT_SQLITE_PATH') is relative and would not persist across restarts; rewriting to /config/$CURRENT_SQLITE_PATH" - yq -i -y ".output.sqlite.path = \"/config/${CURRENT_SQLITE_PATH}\"" "$CONFIG_LOCATION" + NEW_SQLITE_PATH="/config/${CURRENT_SQLITE_PATH}" + bashio::log.warning "output.sqlite.path ('$CURRENT_SQLITE_PATH') is relative and would not persist across restarts; rewriting to $NEW_SQLITE_PATH" + # SQLite does not create missing parent directories, so ensure one exists + # if the (validated, traversal-free) path includes a subdirectory. + mkdir -p "$(dirname "$NEW_SQLITE_PATH")" + yq -i -y ".output.sqlite.path = \"${NEW_SQLITE_PATH}\"" "$CONFIG_LOCATION" fi yq -i -y '.output.sqlite.path //= "/config/birdnet.db"' "$CONFIG_LOCATION" diff --git a/birdnet-go/CHANGELOG.md b/birdnet-go/CHANGELOG.md index 4e4ba7dc7d..5c68e6b045 100644 --- a/birdnet-go/CHANGELOG.md +++ b/birdnet-go/CHANGELOG.md @@ -1,3 +1,6 @@ +## nightly-20260615-3 (05-07-2026) +- Harden the `output.sqlite.path` rewrite added for the persistence fix: reject paths containing `..` traversal segments (shared `validate_safe_path` check), and create the destination's parent directory under `/config` when the relative path includes a subdirectory (e.g. `db/birdnet.db`), since SQLite does not create missing parent directories itself. + ## nightly-20260615-2 (05-07-2026) - Minor bugs fixed - Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774) diff --git a/birdnet-go/config.yaml b/birdnet-go/config.yaml index 7799a3bfe1..90dbdcee7d 100644 --- a/birdnet-go/config.yaml +++ b/birdnet-go/config.yaml @@ -128,4 +128,4 @@ slug: birdnet-go udev: true url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go usb: true -version: "nightly-20260615-2" +version: "nightly-20260615-3" diff --git a/birdnet-go/rootfs/etc/cont-init.d/01-structure.sh b/birdnet-go/rootfs/etc/cont-init.d/01-structure.sh index 65b44a633b..c0e3493064 100755 --- a/birdnet-go/rootfs/etc/cont-init.d/01-structure.sh +++ b/birdnet-go/rootfs/etc/cont-init.d/01-structure.sh @@ -17,12 +17,22 @@ normalize_path() { # Reject paths containing characters that would break the SQL/YAML literals # we substitute into below. We deliberately allow only "safe" filename chars. +# Also reject ".." path segments so a relative path can't traverse outside +# the directory we prefix it with (e.g. "/config") when we rewrite it below. validate_safe_path() { local p="$1" if [[ ! "$p" =~ ^[A-Za-z0-9._/-]+$ ]]; then bashio::log.fatal "Refusing unsafe path: '$p' (only [A-Za-z0-9._/-] allowed)" exit 1 fi + local segment + local IFS=/ + for segment in $p; do + if [[ "$segment" == ".." ]]; then + bashio::log.fatal "Refusing unsafe path: '$p' (path traversal '..' is not allowed)" + exit 1 + fi + done } if [ ! -f "$CONFIG_LOCATION" ]; then @@ -145,8 +155,12 @@ bashio::log.info "Seeding default configuration values (only if missing)" CURRENT_SQLITE_PATH="$(yq -r '.output.sqlite.path // ""' "$CONFIG_LOCATION")" if [[ -n "$CURRENT_SQLITE_PATH" && "$CURRENT_SQLITE_PATH" != /* ]]; then validate_safe_path "$CURRENT_SQLITE_PATH" - bashio::log.warning "output.sqlite.path ('$CURRENT_SQLITE_PATH') is relative and would not persist across restarts; rewriting to /config/$CURRENT_SQLITE_PATH" - yq -i -y ".output.sqlite.path = \"/config/${CURRENT_SQLITE_PATH}\"" "$CONFIG_LOCATION" + NEW_SQLITE_PATH="/config/${CURRENT_SQLITE_PATH}" + bashio::log.warning "output.sqlite.path ('$CURRENT_SQLITE_PATH') is relative and would not persist across restarts; rewriting to $NEW_SQLITE_PATH" + # SQLite does not create missing parent directories, so ensure one exists + # if the (validated, traversal-free) path includes a subdirectory. + mkdir -p "$(dirname "$NEW_SQLITE_PATH")" + yq -i -y ".output.sqlite.path = \"${NEW_SQLITE_PATH}\"" "$CONFIG_LOCATION" fi yq -i -y '.output.sqlite.path //= "/config/birdnet.db"' "$CONFIG_LOCATION"