Skip to content

1.6.1: execute_ruby process-execution hardening

Choose a tag to compare

@mariochavez mariochavez released this 04 Aug 16:37
f4ad8ea

Security patch for the execute_ruby tool. Closes a command-execution path reported by a security researcher, tightens the static filter, and drops the misleading "read-only sandbox" framing.

Security

  • execute_ruby process-execution hardening — closed a command-execution path and tightened the static filter:
    • require restricted to a tiny data-lib allowlist: require_relative and dynamic requires are refused, and literal require "lib" is refused except for a small allowlist of pure-data libraries not always preloaded (csv, tzinfo, date, time). Under bin/rails runner the app's models, ActiveRecord, and the stdlib Rails loads on boot are already available, so inspection code needs almost no requires — and every dangerous stdlib escape has to be required first. This closes require "pty" (PTY.spawn/PTY.getpty started a child process outside the Kernel#system guard, giving arbitrary host command execution), along with open3, fiddle, ffi, and socket, at the source rather than by enumerating individual APIs. (tzinfo pairs with the existing system-timezone read-path allowlist so Time.zone code keeps working.)
    • Native/PTY patterns: PTY, Fiddle, and FFI are added to the forbidden-pattern scan as defense in depth.
    • Dynamic dispatch to execution sinks hard-blocked: send/public_send/__send__/const_get aimed by name at an execution or eval sink (system, exec, spawn, fork, eval, popen, Open3, Process, PTY, …) are now rejected outright instead of merely gated behind confirm_risky. Benign dynamic dispatch (e.g. record.send(:name)) is unaffected.
    • Honest framing: the tool description and docs no longer call execute_ruby a read-only sandbox. It runs caller-supplied Ruby with the privileges of the server process; the controls are best-effort guardrails, not an isolation boundary. execute_ruby is operator-started, usually local, against the operator's own project — so the practical risk is unintended execution (e.g. a prompt-injected coding agent), which runs with the operator's privileges. For a real boundary, run the server against a read-only database user and/or inside OS-level isolation.

Fixed

  • ReDoS in the execute_ruby static scan: Rewrote the require/dynamic-dispatch matchers to remove an ambiguous \s*\(?\s* construct that backtracked in polynomial time on adversarial whitespace input (CodeQL alert). Matching is now linear.

Upgrade notes

Behavior change for execute_ruby callers: snippets that require a non-allowlisted library, use require_relative, or reach an execution sink via send/const_get are now rejected. Normal inspection code (model queries, read_file/list_files, Time.zone, CSV) is unaffected — under bin/rails runner the environment it needs is already loaded.


Full changelog: https://github.com/maquina-app/rails-mcp-server/blob/v1.6.1/CHANGELOG.md