Aikido

Even gem list can run someone else's code

npm made install scripts opt-in. Here's where RubyGems stands, and how to lock down your own installs.

Written by
Hunter Schwartz

If your team runs gem install or bundle install, on laptops or in CI, this post is for you. It’s the kind of thing you’d never expect. A gem can run code on your machine without an install script. Here’s how to check for four data-exfiltrating gems we found on RubyGems, turn this behavior off with new settings, and see what Aikido flags in your feed.

TL;DR

  • Installing a Ruby gem can run that gem's code before your app loads it. Native extension build scripts run during install. RubyGems plugins run at the end of gem install and again on every later gem command, even gem list or gem -v.
  • npm already addresses this issue. npm 12 made install scripts opt-in, including native builds. RubyGems still runs extension builds and plugins by default.
  • Stable RubyGems and Bundler 4.0.x have no way to turn either behavior off. The 4.1 prereleases add opt-out settings, but they’re off by default, so you have to turn them on yourself.
  • Aikido's new detection rules found four gems on RubyGems, published between 2013 and 2015, that send the installer's username, a macOS screenshot, or a beacon to a remote server. The reported gems are still installable as of November 4, 2026.
  • If you’re an Aikido customer, Aikido flags these gems as a malware issue in your feed.
  • We reported the extension, plugin execution paths, and the four data-exfiltrating gems we found to the RubyGems security team in October.

What to do if you run Ruby gems

If you run Ruby gems, follow these steps to make sure your system is secured. Otherwise, you can skip this section to learn more about how these features work.

1. Check for the four malicious gems. In each project, search every lockfile under the current directory:

grep -rE '^ +(xmastree|ancient-city-ruby|hacky|et_phone_home) \(' --include=Gemfile.lock .

To check installed gems without running a gem command (which would load plugins), run:

ruby -e 'Gem.path.each { |p| puts Dir.glob("#{p}/gems/*") }' | grep -E '/(xmastree|ancient-city-ruby|hacky|et_phone_home)-[0-9]'

If you use rbenv or another Ruby version manager, repeat this for each Ruby version. Affected versions are in the IOC table at the end of this post.

What to do if you installed one of these gems. The payload runs from a Rakefile (native extension) during install, so installing the gem was enough for it to execute.

  • Stop any processes the gem started.
  • Uninstall the gem. Replace GEM_NAME with the affected gem’s name: gem uninstall GEM_NAME --all --executables
  • Remove the gem from your project dependencies. Remove it from your Gemfile, then run bundle install to regenerate the Gemfile.lock.
  • Assess potential exposure. Review the gem’s actions in the IOC section to determine what may have been exfiltrated, and take appropriate action.

2. Review which RubyGems plugins you have installed and uninstall unneeded plugins. Run the command below and list each directory it prints. RubyGems loads plugins from every gem path, not just the default one.

ruby -e 'Gem.path.each { |p| puts Gem.plugindir(p) }'

3. Turn off extension builds and plugin installs where you can. This needs RubyGems and Bundler 4.1.0.beta3. Install it with gem update --system 4.1.0.beta3, which updates Bundler too. (gem update --system --pre does not install it.)

  • For gem install, pass --no-build-extension --no-install-plugin.
  • For Bundler, run bundle config set --global no_build_extension true and bundle config set --global no_install_plugin true.
  • In CI, set BUNDLE_NO_BUILD_EXTENSION=true and BUNDLE_NO_INSTALL_PLUGIN=true. Only turn off extension builds in a CI job that doesn't load the app, such as a dependency install and audit step, unless every gem in your bundle has a precompiled build. Otherwise your app and tests won't be able to load gems with skipped extensions.

These settings only stop code from running during install. A malicious gem’s code still runs when your app or tests load it, so they protect install-only steps, not anything that requires the bundle.

4. Expect gems with skipped extensions to stop loading. RubyGems won't load a gem whose declared extensions weren't built, even if its native code is optional, and prints Ignoring <gem> because its extensions are not built. Bundler treats the gem as missing, so bundle exec fails with Could not find <gem> in locally installed gems, and every bundle install reinstalls it.

Precompiled platform builds don't declare extensions so they still load. Once you've reviewed a gem, bring its extension back:

  • gem install: run gem pristine <gem> --extensions. This also installs the gem's plugin, if it has one, and that plugin then runs. To install only the plugin, run gem pristine <gem> --only-plugins.
  • Bundler: unset no_build_extension and run bundle install again. Keep no_install_plugin set if you want plugins to stay off. This rebuilds every gem in the bundle whose extensions were skipped. Bundler's own warning says to run bundle pristine <gem>, but in 4.1.0.beta3 that fails with the same Could not find error, because Bundler doesn't see the gem as installed. bundle pristine does work for a gem that only had its plugin skipped, though it rebuilds the extension too.

5. Inspect new or recently updated gems before installing them. In a clean environment, run gem fetch and then gem unpack. Read extconf.rb, mkrf_conf.rb, any Rakefile or CMake file listed under extensions in the gemspec, and any lib/rubygems_plugin.rb.

We tested these commands on Ruby 4.0.9 with RubyGems and Bundler 4.0.23 (the current stable release) and 4.1.0.beta3. The opt-out flags, settings, and recovery steps only apply to 4.1.0.beta3.

Two ways a gem runs code on your machine

Ruby has two ways to run a gem's code without anything resembling an install script: Native Extensions and Ruby Gems Plugins. Both let an attacker run a payload before a developer starts the application. A malicious extension build script could steal credentials during installation. A malicious RubyGems plugin could do the same at the end of that same gem install, and again every time the developer runs a gem command.

Method 1: Native extensions

A gem declares extensions in its gemspec (the gem's manifest). If one of those is an extconf.rb build script, RubyGems runs that Ruby file during installation, then runs the Makefile it generates. Whatever Ruby is in that file runs on install, the same way node-gyp runs native builds for npm packages.

RubyGems also runs mkrf_conf.rb as a Ruby script during install if a gem lists one under extensions, then runs rake in the same directory. Besides these Ruby scripts, RubyGems supports builds with configure, Rakefile, CMake, and Cargo, each running code through its own build tool.

What an attacker adds to a gem:

  • One file, extconf.rb or mkrf_conf.rb (for example, ext/foo/extconf.rb), containing any Ruby they want
  • One line in the gemspec: spec.extensions = ["ext/foo/extconf.rb"]

Why it's easy to slip in: Many legitimate gems, such as bcrypt and bootsnap, use extconf.rb. It looks like any other build file, and there's no separate "scripts" section to draw a reviewer's eye.

Method 2: RubyGems plugins

If a gem includes a file named rubygems_plugin.rb, RubyGems installs it as a plugin. At install time, RubyGems writes a small loader for that file into its plugins directory, and every later gem command loads what's in that directory.

What an attacker adds to a gem: One file named exactly rubygems_plugin.rb at the root of a declared require path, normally lib/rubygems_plugin.rb.

When it runs:

  • RubyGems loads a newly installed plugin immediately at the end of gem install (if no other version is installed).
  • It subsequently loads every installed plugin during any gem command, including read-only commands like gem list, gem search, and gem -v, as well as commands concerning unrelated gems.
  • The plugin executes repeatedly on each command for as long as the gem remains installed.

Why it's easy to slip in: It's a file in lib/ with a special name. No separate plugin declaration is required, so a reviewer looking for explicit execution hooks could miss it. RubyGems reports plugin-loading exceptions as warnings, so the command keeps going after a plugin fails.

Gems that Bundler installs into vendor/bundle or a custom BUNDLE_PATH work a little differently. A plain gem list doesn't load their plugins, but bundle exec gem list does.

npm has already been through this

npm learned the hard way that install-time security has to cover more than just lifecycle scripts. In the Miasma campaign, attackers moved their execution trigger from package.json into binding.gyp, npm's native build file, which node-gyp runs automatically on install. Checks that only watched package.json install hooks missed it. npm 12 responded by making dependency installation scripts opt-in, including node-gyp builds.

Ruby has the same problem, and we don’t have a stable release that fixes it. An extconf.rb looks like an ordinary build configuration, and a rubygems_plugin.rb needs no declaration at all, so neither looks like an install script to review. Attackers are already targeting Ruby developers, too. In the SleeperGem campaign, attackers published malicious gem versions from what appear to be compromised dormant maintainer accounts.

1 in 13 popular gems runs code at install

On October 26, 2026, we pulled the top ~1,000 gems by total downloads from bestgems.org and read their published gemspecs to understand how common native extensions and plugin files are today. Keep in mind, whether or not gems use these install time mechanisms now does not measure risk, because attackers can use them at any time. It does however show how normal these build steps look to a reviewer.

  • Native extensions: 74 of 997 (about 1 in 13) declare extension build steps, including bcrypt, bigdecimal, bootsnap, bson, debug, and many common database and parsing gems.
  • Plugin files: 2 of 997 (rdoc and yard) ship rubygems_plugin.rb. Both load on later gem commands when installed in the active environment.

Four proof-of-concept gems that send data home are still installable

While testing our detection rules, we found four gems, published between 2013 and 2015, that run code on install: xmastree, ancient-city-ruby, hacky, and et_phone_home. They send the installer's username, a silent macOS screenshot, or a simple beacon to a remote server.

They look like proofs of concept or research tests, but they still run on install and send data off the machine. Affected versions and destinations are in the IOC table at the end of this post.

Disclosure timeline

  • October 27, 2026: We reported the extension and plugin execution paths to the RubyGems security team. They followed up with questions about the opt-out flags and haven't commented further.
  • October 30, 2026: We reported the four gems to the RubyGems security team.
  • November 4, 2026: The reported gems are still available on RubyGems.

Stable RubyGems has no off switch

You might think you can just pass the flags --no-build-extension and --no-install-plugin to gem. However both of these flags only exist in the 4.1 prereleases, and even in 4.1.0.beta3 both behaviors stay on unless you turn them off. On stable 4.0.x, gem install rejects them as an invalid option.

Install pathControls for extension builds and plugin installs
gem install, stable 4.0.xNone. The flags don't exist.
bundle install, stable 4.0.xNone. The settings don't exist.
gem install, 4.1.0.beta3--no-build-extension and --no-install-plugin flags. Both behaviors stay on unless you pass them.
bundle install, 4.1.0.beta3no_build_extension and no_install_plugin settings in bundle config. Both behaviors stay on unless you set them.

How Aikido detects this

If you’re an Aikido user, check your central feed and filter on malware issues. When a malicious gem shows up in one of your repos’ lockfiles, Aikido flags it as a malware issue. Aikido rescans nightly, but we recommend triggering a manual rescan immediately if you think you may be affected.

To stop known malicious gems before they reach a developer’s machine, use Aikido Device Protection. It checks gem install and bundle install on Ruby 3 and later against Aikido Intel, and blocks known malware before it touches the filesystem. You can roll it out through your mobile device management (MDM) tool.

IOCs

GemVersionFlagged by AikidoWhat it does
xmastree0.0.2YesRakefile in extensions. POSTs whoami output to http://cryptic-ocean-8847.herokuapp.com/usernames on install.
xmastree0.0.1YesSame payload and URL, but extensions is empty, so it doesn't run on install. It only runs if someone runs rake inside the gem directory.
ancient-city-ruby2013.1YesRakefile in extensions. POSTs whoami output to http://stark-samurai-8122.herokuapp.com/logs, then deletes itself.
hacky0.0.2 (only version)YesRakefile in extensions. Takes a silent screenshot, uploads it to http://appfolio.herokuapp.com/upload on install, then deletes the screenshot and itself. Only works on macOS.
et_phone_home0.1.0 (only version)YesRakefile in extensions. Sends a GET request to http://176.31.142.25/ruby_no_way_you_got_here_randomly on install.
Share:

https://www.aikido.dev/blog/gem-list-runs-someone-elses-code

Scan for malware

Start for Free
4.7/5
Tired of false positives?

Try Aikido like 100k others.
Start Now
Get a personalized walkthrough

Trusted by 100k+ teams

Book Now
Scan your app for IDORs and real attack paths

Trusted by 100k+ teams

Start Scanning
See how AI pentests your app

Trusted by 100k+ teams

Start Testing

Get secure today,
quickly and for free.

Secure your code, cloud, and runtime in one central system.
Connect a repo to discover what the reasoning agents find in your codebase.

No credit card required | Scan results in 32 seconds.
Trusted by 150k+ orgs