Banked: 2026-07-14, cycle299 (aws/language-servers GhostApproval incomplete-fix).
A path-containment guard is retrofitted to canonicalize via realpath before the boundary check (the correct fix for symlink-name-vs-target confusion). But realpath THROWS on a path whose final target does not exist yet (a dangling symlink, or a not-yet-created file). The fallback then reconstructs the path from realpath(parent) + basename(input) - which uses the LINK's OWN NAME, not the link's target. So:
- symlink -> existing outside target: realpath succeeds -> resolves outside -> guard fires. (fix works)
- symlink -> non-existent outside target (dangling): realpath ENOENT -> fallback = in-boundary link name -> guard says "inside, allow" -> the actual write follows the link OUTSIDE. (bypass)
The create-new-file case is exactly the high-value one (create ~/.ssh/authorized_keys, ~/.bashrc), so the incomplete fix re-enables the flagship attack.
Any guard with this shape:
try { canonical = realpath(input) }
catch (ENOENT) { canonical = join(realpath(dirname(input)), basename(input)) } // <-- uses link NAME
if (isInside(boundary, canonical)) allow_without_prompt
The fallback never does lstat/readlink on the final component to detect that input is ITSELF a symlink. A file that "doesn't exist yet" and a symlink whose "target doesn't exist yet" are conflated.
Resolve-as-far-as-possible: walk and resolve existing ancestors AND the final-component symlink (even if its target is dangling), then append the non-existent tail; run the boundary/sensitive check against the RESOLVED target. Defense-in-depth: O_NOFOLLOW / lstat-reject on the write itself.
When a project ships a realpath-based containment fix, ALWAYS test the dangling-symlink / create-new-file case, not just the existing-target case. The disclosed CVE + its PoC almost always use an existing target; the create case is the residual.
Sibling of: - cycle240 (File Browser: RemoveAll skips the guard other methods call - guard-skip). - cycle298-6 (Forgejo mirror: mechanism-divergent partial port - fix reaches main path, misses wiki path). - cycle299 (this: canonicalize-existing-only - fix resolves existing target, misses dangling). Unifying principle: a fix covers the common path/state and misses a sibling path/state. Enumerate the states (exists vs dangling vs new; main vs wiki; method A vs sibling method B) and test each.
Disclosure-gate must include VENDOR STANCE: a real guard-bypass on a vendor that DISPUTES the class (e.g. Anthropic/Claude Code treats trusted-dir+approved-prompt as out-of-threat-model) is not creditable. Route incomplete-fix effort to accept-the-class vendors (Amazon/Cursor/Google here).