Skip to content

Commit e4eacf8

Browse files
fix(security): contain offline pack artifacts
1 parent beef04a commit e4eacf8

2 files changed

Lines changed: 79 additions & 4 deletions

File tree

scripts/lib/security.sh

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -602,14 +602,61 @@ acfs_offline_pack_path_is_safe() {
602602
local rel_path="${1:-}"
603603

604604
case "$rel_path" in
605-
""|.|..|/*|../*|*/..|*"/../"*|*"/./"*|*"/")
605+
""|.|..|/*|./*|../*|*/..|*"/../"*|*"/./"*|*"/")
606606
return 1
607607
;;
608608
esac
609609

610610
return 0
611611
}
612612

613+
acfs_offline_pack_resolve_existing_path() {
614+
local path="${1:-}"
615+
local realpath_bin=""
616+
local dir=""
617+
local base=""
618+
619+
[[ -n "$path" && -e "$path" ]] || return 1
620+
621+
realpath_bin="$(acfs_security_system_binary_path realpath 2>/dev/null || true)"
622+
if [[ -n "$realpath_bin" ]]; then
623+
"$realpath_bin" -e -- "$path"
624+
return $?
625+
fi
626+
627+
if [[ -d "$path" ]]; then
628+
(cd "$path" 2>/dev/null && pwd -P)
629+
return $?
630+
fi
631+
632+
case "$path" in
633+
*/*)
634+
dir="${path%/*}"
635+
base="${path##*/}"
636+
;;
637+
*)
638+
dir="."
639+
base="$path"
640+
;;
641+
esac
642+
643+
(cd "$dir" 2>/dev/null && printf '%s/%s\n' "$(pwd -P)" "$base")
644+
}
645+
646+
acfs_offline_pack_artifact_is_contained() {
647+
local pack_root="${1:-}"
648+
local artifact_file="${2:-}"
649+
local pack_root_real=""
650+
local artifact_real=""
651+
652+
pack_root_real="$(acfs_offline_pack_resolve_existing_path "$pack_root" 2>/dev/null || true)"
653+
artifact_real="$(acfs_offline_pack_resolve_existing_path "$artifact_file" 2>/dev/null || true)"
654+
655+
[[ -n "$pack_root_real" && "$pack_root_real" != "/" ]] || return 1
656+
[[ -n "$artifact_real" ]] || return 1
657+
[[ "$artifact_real" == "$pack_root_real/"* ]]
658+
}
659+
613660
acfs_offline_pack_validate_manifest() {
614661
local pack_root="$1"
615662
local manifest_file="$2"
@@ -792,7 +839,15 @@ acfs_offline_pack_verify_artifact() {
792839
fi
793840

794841
artifact_file="$pack_root/$rel_path"
795-
if [[ ! -f "$artifact_file" || -L "$artifact_file" ]]; then
842+
if [[ ! -f "$artifact_file" ]]; then
843+
acfs_offline_pack_error "pack_unbundled_required_module" "$name" "artifact file is missing or unsafe: $rel_path"
844+
return 1
845+
fi
846+
if ! acfs_offline_pack_artifact_is_contained "$pack_root" "$artifact_file"; then
847+
acfs_offline_pack_error "pack_path_escape" "$name" "artifact path resolves outside the pack: $rel_path"
848+
return 1
849+
fi
850+
if [[ -L "$artifact_file" ]]; then
796851
acfs_offline_pack_error "pack_unbundled_required_module" "$name" "artifact file is missing or unsafe: $rel_path"
797852
return 1
798853
fi

tests/unit/test_offline_artifact_pack_consumer.sh

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,25 @@ write_pack() {
6969
local expires_at="$2"
7070
local arch="$3"
7171
local include_artifact="${4:-yes}"
72+
local artifact_rel="${5:-artifacts/fixture.module/${TOOL}-install.sh}"
7273
local output_dir="$TEST_ROOT/$name"
7374
local pack_root="$output_dir/acfs-offline-pack"
74-
local artifact_rel="artifacts/fixture.module/${TOOL}-install.sh"
7575
local artifact_path="$pack_root/$artifact_rel"
7676
local artifact_size=""
7777
local artifacts_json="[]"
7878

79-
mkdir -p "$pack_root/artifacts/fixture.module"
79+
mkdir -p "$pack_root/artifacts"
8080
write_checksums "$pack_root/checksums.yaml" "$ARTIFACT_SHA"
8181

8282
if [[ "$include_artifact" == "yes" ]]; then
83+
mkdir -p "${artifact_path%/*}"
8384
printf '%s' "$CONTENT" > "$artifact_path"
8485
artifact_size="$(acfs_security_file_size "$artifact_path")"
86+
elif [[ "$include_artifact" == "manifest-only" ]]; then
87+
artifact_size="$(printf '%s' "$CONTENT" | wc -c | tr -d '[:space:]')"
88+
fi
89+
90+
if [[ "$include_artifact" == "yes" || "$include_artifact" == "manifest-only" ]]; then
8591
artifacts_json="$(
8692
jq -n \
8793
--arg id "fixture.module:$TOOL" \
@@ -232,6 +238,19 @@ test_missing_artifact_is_refused() {
232238
expect_refusal_code "missing_artifact_is_refused" "$pack_root" "pack_unbundled_required_module"
233239
}
234240

241+
test_symlink_parent_escape_is_refused() {
242+
local pack_root=""
243+
local outside_dir="$TEST_ROOT/outside-artifacts"
244+
local artifact_rel="artifacts/escape-parent/${TOOL}-install.sh"
245+
246+
pack_root="$(write_pack "symlink-parent-escape" "$FUTURE_EXPIRES" "$CURRENT_ARCH" manifest-only "$artifact_rel")"
247+
mkdir -p "$outside_dir"
248+
printf '%s' "$CONTENT" > "$outside_dir/${TOOL}-install.sh"
249+
ln -s "$outside_dir" "$pack_root/artifacts/escape-parent"
250+
251+
expect_refusal_code "symlink_parent_escape_is_refused" "$pack_root" "pack_path_escape"
252+
}
253+
235254
test_unsupported_arch_is_refused() {
236255
local pack_root=""
237256

@@ -301,6 +320,7 @@ run_all_tests() {
301320
test_stale_pack_is_refused
302321
test_tampered_artifact_is_refused
303322
test_missing_artifact_is_refused
323+
test_symlink_parent_escape_is_refused
304324
test_unsupported_arch_is_refused
305325
test_missing_pack_fails_closed
306326
test_live_path_still_works_without_pack

0 commit comments

Comments
 (0)