Skip to content

1.4.9-beta release - #27

Merged
Core447 merged 10 commits into
mainfrom
dev
Apr 10, 2024
Merged

Core447 merged 10 commits into
mainfrom
dev

Conversation

@Core447

@Core447 Core447 commented Apr 10, 2024

Copy link
Copy Markdown
Member

1.4.9-beta

@Core447
Core447 merged commit 75d636d into main Apr 10, 2024
nazbert added a commit to nazbert/Deckard that referenced this pull request Jul 12, 2026
…talls

plugin_id/icon_id/wallpaper_id come from a remote manifest.json and were
joined as path components with no validation -- an id like "../../.."
walked shutil.rmtree/install targets out of the app's data dirs
(install_plugin's directory reset, uninstall_icon/-wallpaper/-sd_plus's
rmtree), and an absolute id replaced the base dir entirely. Icon and
wallpaper packs are data-only: a hostile pack got filesystem-wide delete
with no code execution involved.

StoreBackend.is_safe_asset_id now whitelists ids (^[A-Za-z0-9]
[A-Za-z0-9._-]{0,127}$) at every id->path join site, REJECTING on
mismatch (400) rather than normalizing -- a traversal id is a hostile or
broken manifest, not something to quietly repair. The read-only
get_local_sha probes in prepare_* go through the same guard
(get_local_sha_for_id) so an evil catalog entry can't even probe the
filesystem, and a manifest without an id no longer TypeErrors the join.

The __install__.py / requirements.txt runners also ran through
subprocess.run(f"{sys.executable} {path}", shell=True), which broke on
spaces in the data path and allowed shell injection via crafted path
components; both now run as argv lists without a shell.

Marked breaking (!) because install/uninstall of assets whose manifest id
fails the whitelist is now refused instead of acted on.

Covered by tests/scenario_store_id_sanitizer.py (network-free).

Closes StreamController#27
nazbert added a commit to nazbert/Deckard that referenced this pull request Jul 12, 2026
…a; zip-slip guard

Review round 1 follow-up to the StreamController#27 hardening: close the shell-injection
sibling the id/pip de-shelling left behind, and add belt-and-suspenders
archive validation.

clone_repo (the --devel, non-flatpak install path) ran the remote store
catalog's `commit_sha` and `branch` through os.system() f-strings:

    os.system(f"cd '{local_path}' && git switch {branch_name}")

`branch` is plugin["branch"] straight from the catalog with no validation,
so a catalog entry `branch: "main; <cmd>"` injected a shell -- the exact
class the id/pip runners were de-shelled to prevent. All three os.system
git calls (pull / reset --hard / switch) now run as argv lists via
`git -C <dir> ...` with no shell. The remote-sourced values are also
validated defensively before use: `is_safe_ref_name` rejects shell
metachars / whitespace / newlines / leading '-' (git-option smuggling),
and `is_safe_commit_sha` enforces the 40-hex form; a bad value returns 400
before any git runs.

Also adds `zip_has_unsafe_members` as defense-in-depth before
shutil.unpack_archive: GitHub .zip archives are trusted and CPython's
zipfile already strips `..`/absolute members, but an explicit check means a
future archive-source or format change (tar, which CPython does NOT
sanitize identically) can't silently reintroduce a Zip-Slip write.

Marked `!` for consistency with the StreamController#27 commit: a catalog whose branch/sha
does not conform is now refused rather than executed.

Tests (scenario_store_id_sanitizer): ref/sha validator cases; clone_repo
refuses an injected branch/sha (400, no git call, injected side-effect
marker never created) and only ever invokes git as an argv list (os_sys
must not be reached); zip_has_unsafe_members flags traversal + absolute
members and passes a clean archive. Red-tested: reverting to os.system
trips the shell-use guard; neutering the zip guard fails the slip test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant