# Runtime expansion bypasses literal validation

Validation checks a literal value, but variables, symlinks or path resolution change it before use

_Area: Software supply chain and CI/CD. Seen in 3 disclosed reports; latest 2026-09-30. Updated 2026-11-02._

## How it works
A check runs on a value as written, but the value is not used as written. Something later — variable interpolation, path resolution, a cached "already validated" prefix, or a directory-discovery loop — turns the checked literal into a different effective value. The check and the use see different strings or paths, so the invariant the check believed it enforced never holds at the point of use. The root assumption that breaks: "the value I validated is the value that gets used."

## Where it shows up
- Config validation that runs before variable interpolation (CI keys, cache paths, artifact names) [E3](https://bugbountylibrary.com/r/h1%3A409395.md)
- Path stacks or caches that mark prefixes "validated" and skip re-checks on later transitions [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-f89h-2fjh-2r9q.md)
- Directory-discovery loops that test candidate paths in an order where a weaker check passes first [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-239g-whfq-7xj9.md)
- Checks that test file existence but not file content (e.g. a HEAD file never parsed) [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-239g-whfq-7xj9.md)
- Any place a literal is normalized/validated at parse time but expanded at execution time [E3](https://bugbountylibrary.com/r/h1%3A409395.md)

## How to review
Find every value that is validated and then used. Ask: can this value be re-expanded, re-resolved, or re-interpreted between check and use? The property that must hold: the exact bytes/path validated are the exact bytes/path consumed, or validation is repeated after expansion. For path stacks, confirm the fast path performs the same symlink and collision checks as the full path [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-f89h-2fjh-2r9q.md). For discovery loops, confirm each candidate check validates content, not just existence, and that the strongest check is not ordered last [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-239g-whfq-7xj9.md). For config, confirm interpolation happens before validation, not after [E3](https://bugbountylibrary.com/r/h1%3A409395.md).

## How finders spotted it
- Read the discovery loop ordering and noticed `.git` was tested last while earlier checks could be satisfied by ordinary tracked files; then traced hook path resolution to confirm [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-239g-whfq-7xj9.md).
- Read the prefix-cache fast path and saw the leaf-to-directory transition skipped the symlink/unlink checks that lived only in the full push path; then crafted duplicate entries to exercise it [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-f89h-2fjh-2r9q.md).
- Noticed a prior fix validated only the literal YAML key, hypothesized interpolation happens after validation, and tested by setting the key via a variable [E3](https://bugbountylibrary.com/r/h1%3A409395.md).

## Variants seen
- Directory-discovery ordering lets tracked files shadow the real `.git` [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-239g-whfq-7xj9.md)
- Cached validated prefix skips symlink checks on directory transition [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-f89h-2fjh-2r9q.md)
- Variable interpolation resolves a traversal path after literal validation [E3](https://bugbountylibrary.com/r/h1%3A409395.md)

## Impact and severity
All three reached code execution: hooks run on commit [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-239g-whfq-7xj9.md), symlinks written into `.git/hooks` or PATH dirs [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-f89h-2fjh-2r9q.md), cache poisoning crossing project boundaries [E3](https://bugbountylibrary.com/r/h1%3A409395.md). Severity is raised by chaining — file read/write via tracked config [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-239g-whfq-7xj9.md), symlink-to-hook escalation [E2](https://bugbountylibrary.com/r/ghsa%3AGHSA-f89h-2fjh-2r9q.md), cross-project reach [E3](https://bugbountylibrary.com/r/h1%3A409395.md).

## Why fixes fail
The original slash fix validated the literal key only; the variable path bypassed it, so the fix was incomplete until validation moved after interpolation [E3](https://bugbountylibrary.com/r/h1%3A409395.md).

## Seen in real software
Open-source packages and projects, and products, where this pattern was found. If the target runs one, compare the version it runs with the fix.
- **gitpython (pip)** <= 3.1.59, fixed in 3.1.60: GitPython: Repository content can impersonate the git directory, leading to arbitrary code execution (CVE-2026-87817). Report: https://bugbountylibrary.com/r/ghsa%3AGHSA-239g-whfq-7xj9.md. Fix: https://github.com/gitpython-developers/GitPython/commit/c7cf4d13b1ed0a2e70f2a1f3c6b4fc6c2652cf0b
- **gix-fs (rust)** <= 0.21.0, fixed in 0.21.1: gix-fs: Symlink prefix-reuse allows worktree escape during checkout (CVE-2026-44471). Report: https://bugbountylibrary.com/r/ghsa%3AGHSA-f89h-2fjh-2r9q.md
- **GitLab**: Bypass of GitLab CI runner slash fix in YAML validation. Report: https://bugbountylibrary.com/r/h1%3A409395.md

## Evidence
- E1: https://bugbountylibrary.com/r/ghsa%3AGHSA-239g-whfq-7xj9.md
- E2: https://bugbountylibrary.com/r/ghsa%3AGHSA-f89h-2fjh-2r9q.md
- E3: https://bugbountylibrary.com/r/h1%3A409395.md
