# Unpacker improperly validates symlinks, allowing gems writes to arbitrary locations

Crafted gem with a symlink pointing outside the install dir bypasses RubyGems' path check, letting `gem install`/`gem unpack` write arbitrary files like /tmp/HACKED.

- Payout: hidden
- Severity: medium
- Program: RubyGems
- Found by: nmalkin
- Disclosed: 2018-12-31
- Write-up quality: 7/10
- Original: https://hackerone.com/reports/270072
- Library page: https://bugbountylibrary.com/r/h1%3A270072
- Tags: area:oss domain:oss impact:integrity impact:rce kind:bounty platform:hackerone sev:medium tech:ruby technique:source-review vuln:path-traversal

**Target:** RubyGems package unpacker/installer (lib/rubygems/package.rb install_location)
**Root cause:** install_location resolves the destination dir with File.realpath and checks the target filename .start_with? that dir, but a symlink's own path is local while a file using it as prefix passes the check, so writes escape via the symlink target.
**Steps:**
1. Build a gem whose data.tar.gz contains `README`, a symlink `link -> /tmp`, and a file `link/HACKED`.
2. Run `gem install symlink.gem` or `gem unpack symlink.gem`.
3. Observe `/tmp/HACKED` created outside the install directory.
**How found:** Reading install_location's realpath + start_with? check and reasoning that a symlink's destination dir is where it lives, not where it points, so a file prefixed by the symlink passes the check yet escapes; noted the 2015 symlink restriction was later loosened.
**Exploitation:** Attacker distributes a crafted gem; on install/unpack the symlink redirects the write of `link/HACKED` to an arbitrary path with arbitrary name, contents and permissions, enabling code execution by overwriting a system binary or a user's `.profile`.
**Escalation:** Arbitrary write to any path (e.g. system binary or .profile) escalates to code execution.
**Insight:** When a path-containment check resolves the base dir but only string-prefixes the target, test symlink-plus-child-file combos: the link path is local while the child resolves through it, defeating start_with? checks.

