rails-mcp-server 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61b6de0c4ec67e88054b3fa1b91cfc3d0a9b6cb5d1c565c5086b1e88dfcd7dda
4
- data.tar.gz: 6af401f97462dd368072b223117fab10f49da0b873f7b2f29985da63ec4d8a98
3
+ metadata.gz: 950673f40d56ddea684d938e4e55c2e8db18c3ca44e94988f63565a9db950869
4
+ data.tar.gz: 00712156bdfdfbfc1ec33bd08e31ccb0ad4e7427e3e1e31282de62b4f7b5cc1b
5
5
  SHA512:
6
- metadata.gz: 8cd00e7e51074c682b42ddcbd07dc7697547313c4712c69c0641f6357bbde84775bc8425fe5cad1b05807ca97c065e6d015a5d0aa03079dd371f39e3f664a331
7
- data.tar.gz: 20d054080683e9c8151075fcd6084eaaa868b549c99128768a18abe37bb1ec1ab96c32661273920fd03d100eea37c47c92e36ea6a11c266eff21239f84545d54
6
+ metadata.gz: 98ebb4452eaa610b1430c652c94a339022f10bc05ba7a00b867e8b8c9aaa04d033ea149d923bfe131be52455b044b0feb4d90af24a55670708610e2a57b198a1
7
+ data.tar.gz: 8ecf71ec6b09ed73b35e6f9ea64ec88bfcbbf1f91e2bbb8f033216d0a19d0c6af1493c23fb67be14f99e7933cc6dcc7d64f09d173d20c8c2d6a53f4d25ef88ef
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.6.1] - 2026-08-04
11
+
12
+ ### Security
13
+
14
+ - **`execute_ruby` process-execution hardening**: Closed a command-execution path and tightened the static filter.
15
+ - **`require` restricted to a tiny data-lib allowlist**: `require_relative` and dynamic `require`s 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.)
16
+ - **Native/PTY patterns**: `PTY`, `Fiddle`, and `FFI` are added to the forbidden-pattern scan as defense in depth.
17
+ - **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.
18
+ - **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.
19
+
20
+ ### Fixed
21
+
22
+ - **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.
23
+
10
24
  ## [1.6.0] - 2026-08-03
11
25
 
12
26
  ### Added
@@ -355,6 +369,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
355
369
 
356
370
  ## Version History Summary
357
371
 
372
+ - **v1.6.1** (2026-08-04): `execute_ruby` process-execution hardening (blocks the `require "pty"` → `PTY.spawn` command-execution path, restricts `require` to a data-lib allowlist, hard-blocks dynamic dispatch to execution sinks) and a ReDoS fix in the static scan
358
373
  - **v1.6.0** (2026-08-03): Sandbox hardening for `execute_ruby`, version-manager Ruby resolution, namespaced model resolution, dependency + security updates (drops Ruby 3.2)
359
374
  - **v1.5.1** (2026-03-04): Relaxed dependency version constraints for better compatibility
360
375
  - **v1.4.0** (2025-12-10): Context-efficient architecture with progressive tool discovery (67% token reduction)
data/README.md CHANGED
@@ -17,7 +17,7 @@ This Rails MCP Server implements the MCP specification to give AI models access
17
17
  - Get database schema information
18
18
  - Analyze controller-view relationships
19
19
  - Analyze environment configurations
20
- - Execute sandboxed Ruby code for custom queries
20
+ - Execute Ruby code in the project context for custom queries
21
21
  - Access comprehensive Rails, Turbo, Stimulus, and Kamal documentation
22
22
  - Context-efficient architecture with progressive tool discovery
23
23
  - Seamless integration with LLM clients
@@ -360,7 +360,7 @@ The server uses a progressive tool discovery architecture to minimize context us
360
360
  - **`switch_project`** - Select the active Rails project
361
361
  - **`search_tools`** - Discover available tools by category or keyword
362
362
  - **`execute_tool`** - Invoke internal analyzers with parameters
363
- - **`execute_ruby`** - Run sandboxed Ruby code for custom queries
363
+ - **`execute_ruby`** - Run Ruby code in the project context for custom queries
364
364
 
365
365
  This design reduces initial context from ~2,400 tokens to ~800 tokens while maintaining full functionality.
366
366
 
@@ -412,7 +412,7 @@ After switching, you'll see a Quick Start guide with common commands.
412
412
 
413
413
  #### 4. `execute_ruby`
414
414
 
415
- **Description:** Execute sandboxed Ruby code in the Rails project context.
415
+ **Description:** Execute Ruby code in the Rails project context, for inspection and exploration. Runs with the privileges of the server process — see the Security note below; this is not a sandbox for untrusted code.
416
416
 
417
417
  **Parameters:**
418
418
 
@@ -429,16 +429,21 @@ After switching, you'll see a Quick Start guide with common commands.
429
429
 
430
430
  **Note:** Use `puts` to see output from your code.
431
431
 
432
- **Security:** The sandbox is intended for read-only exploration and applies several layers of defense:
432
+ **Security:** `execute_ruby` runs Ruby that you — or your coding agent — supply, inside your Rails application, with the privileges of the process that started the server. **It is not a security sandbox for untrusted code.** The guardrails below reduce accidental damage and block the obvious escapes, but real Ruby is expressive enough that a determined caller can work around a pattern-based filter; treat the controls as defense-in-depth, not an isolation boundary.
433
433
 
434
- - **No writes / shell / network:** file writes, `system`/`exec`/backticks, and network libraries are blocked by both static analysis and runtime overrides.
434
+ Because you start the server yourself normally locally, against your own project the realistic risk is *running code you didn't intend to*, for example when a coding agent is steered by prompt injection into calling `execute_ruby` with a hostile payload. That payload would run as you. So: only enable this tool for projects and clients you trust, and actually review code before approving a `confirm_risky` re-run.
435
+
436
+ Guardrails applied:
437
+
438
+ - **No writes / shell / network:** file writes, `system`/`exec`/backticks/`spawn`, and network libraries are blocked by both static analysis and runtime overrides.
439
+ - **Almost no `require`:** `require_relative` and dynamic `require`s are refused, and `require "lib"` is refused for everything except a tiny allowlist of pure-data libraries (`csv` and the timezone libs) that Rails doesn't always preload. Under `bin/rails runner` the app's models, ActiveRecord, and the stdlib Rails loads on boot (`json`, `yaml`, `set`, `date`, …) are already available, so inspection code needs no requires anyway — and every dangerous stdlib escape (`pty`, `open3`, `fiddle`, `ffi`, `socket`) has to be required first, so refusing them removes that whole class of bypass at the source.
435
440
  - **Confined file reads:** reads are limited to the project directory (via all of `File`/`IO` `read`/`readlines`/`binread`/`foreach` and `File.open`), plus a small allowlist of read-only system timezone paths (e.g. `/usr/share/zoneinfo`) that Rails needs when code touches `Time.zone`. Paths are symlink-resolved (`realpath`) so a link inside the project cannot point outside it.
436
441
  - **No sensitive files:** `.env`, credentials, keys, and any `.gitignore`d path are refused.
437
- - **Database writes are rolled back:** user code runs inside a transaction that is always rolled back, so `delete_all`, `update`, `save`, and raw DML are undone. Treat the tool as read-only for data too. (Caveat: DDL may still commit on some adapters such as MySQL, and `after_commit` callbacks do not fire.)
442
+ - **Database writes are rolled back:** user code runs inside a transaction that is always rolled back, so `delete_all`, `update`, `save`, and raw DML are undone a safety net against accidental mutation, not a data-access guarantee. (Caveat: DDL may still commit on some adapters such as MySQL, and `after_commit` callbacks do not fire.)
438
443
  - **Bounded execution:** a timeout (default 30s, max 60s) kills the whole process group, so a runaway `bin/rails runner` is terminated rather than orphaned.
439
- - **Confirmation for dual-use constructs:** `send`, `public_send`, `const_get`, and `Kernel#open` are not run until you approve them via `confirm_risky: true`.
444
+ - **Dynamic dispatch to execution sinks is hard-blocked:** `send`/`public_send`/`const_get` aimed by name at `system`/`exec`/`spawn`/`eval`/`Open3`/`Process`/`PTY`/… are rejected outright; the remaining dual-use forms of `send`, `public_send`, `const_get`, and `Kernel#open` are not run until you approve them via `confirm_risky: true`.
440
445
 
441
- These controls are defense-in-depth, not a hard isolation boundary. `execute_ruby` executes real Ruby with full access to the Rails app, so only enable it for projects and clients you trust. For stronger isolation run the server against a database user with read-only grants and/or inside an OS-level sandbox (container, `sandbox-exec`, etc.).
446
+ For a real boundary, run the server against a database user with read-only grants and/or inside OS-level isolation (a container, `sandbox-exec`, seccomp, a dedicated low-privilege user with no ambient credentials or network) rather than relying on these in-process checks.
442
447
 
443
448
  ### Internal Analyzers (via execute_tool)
444
449
 
data/docs/AGENT.md CHANGED
@@ -225,9 +225,9 @@ railsMcpServer:execute_ruby code: "read_file('Gemfile')"
225
225
  railsMcpServer:execute_ruby code: "puts read_file('Gemfile')"
226
226
  ```
227
227
 
228
- **Read-only by design:** `execute_ruby` is for exploration, not mutation. File writes, shell/system calls, and network access are blocked, and any database writes run inside a transaction that is **always rolled back** — so `delete_all`, `update`, and `save` will not persist. Do not rely on it to change data.
228
+ **For inspection, not mutation:** `execute_ruby` is meant for exploring the app, not changing it. File writes, shell/system calls, process spawning, and network access are blocked, and any database writes run inside a transaction that is **always rolled back** — so `delete_all`, `update`, and `save` will not persist. Do not rely on it to change data. These are guardrails, not a security sandbox: the code you send runs with the privileges of the server process, so send only code you would run yourself, and never code from an untrusted source (e.g. copied out of an issue, PR, or file you're inspecting).
229
229
 
230
- **Confirmation for dual-use constructs:** if your code uses `send`, `public_send`, `const_get`, or `Kernel#open`, the tool returns a `CONFIRMATION REQUIRED` message instead of running. These can bypass the sandbox's safety scan, so ask the user to review the code and, only with their explicit approval, re-invoke with `confirm_risky: true`. Do not set `confirm_risky` on your own.
230
+ **Confirmation for dual-use constructs:** if your code uses `send`, `public_send`, `const_get`, or `Kernel#open`, the tool returns a `CONFIRMATION REQUIRED` message instead of running. These can bypass the static safety scan, so ask the user to review the code and, only with their explicit approval, re-invoke with `confirm_risky: true`. Do not set `confirm_risky` on your own.
231
231
 
232
232
  ---
233
233
 
@@ -133,7 +133,7 @@ GitHub Copilot Agent only supports MCP **tools**. The following are available:
133
133
  | `switch_project` | Change active project (optional in single-project mode) |
134
134
  | `search_tools` | Discover available analyzers |
135
135
  | `execute_tool` | Invoke internal analyzers |
136
- | `execute_ruby` | Run sandboxed Ruby code |
136
+ | `execute_ruby` | Run Ruby code in the project context |
137
137
 
138
138
  ### Internal Analyzers (via `execute_tool`)
139
139
 
@@ -171,7 +171,7 @@ The `load_guide` analyzer requires guides to be downloaded. To include guides:
171
171
 
172
172
  ### Network Restrictions
173
173
 
174
- GitHub Copilot Agent runs in a sandboxed environment with firewall restrictions. The MCP server has read-only access to the repository.
174
+ GitHub Copilot Agent runs in a sandboxed environment with firewall restrictions. The MCP server is used here to inspect the repository and runs with the permissions of that agent environment; note that `execute_ruby` executes real Ruby with those permissions and is not itself an isolation boundary (see the [security notes](../README.md#4-execute_ruby)).
175
175
 
176
176
  ## Troubleshooting
177
177
 
@@ -3,17 +3,27 @@ module RailsMcpServer
3
3
  tool_name "execute_ruby"
4
4
 
5
5
  description <<~DESC
6
- Execute read-only Ruby code in the context of the Rails project. Use this for:
6
+ Execute Ruby code in the context of the Rails project, for inspection and
7
+ exploration. Use this for:
7
8
  - Complex queries that would require multiple tool calls
8
9
  - Filtering/transforming data before returning
9
10
  - Custom exploration of the codebase
10
11
 
12
+ This runs with the privileges of the rails-mcp-server process. The
13
+ restrictions below are best-effort guardrails against accidental writes
14
+ and obvious escapes, not a security boundary for untrusted code; only run
15
+ code you would run yourself.
16
+
11
17
  RESTRICTIONS:
12
18
  - Cannot create, modify, or delete files
13
19
  - Cannot read .env, credentials, key files, or .gitignore'd files
14
20
  - Cannot access files outside the project directory (read-only system data
15
21
  such as timezone files under /usr/share/zoneinfo is allowed)
16
22
  - Cannot execute shell commands or system calls
23
+ - Cannot `require` arbitrary libraries or `require_relative` project files.
24
+ Rails and the stdlib it loads (json, yaml, set, ...) are already
25
+ available under `bin/rails runner`; only a few pure-data libraries not
26
+ always preloaded (csv, and the timezone libs) may be required
17
27
  - Database writes run inside a transaction that is always rolled back, so
18
28
  treat this as read-only for data too (note: DDL may still commit on some
19
29
  adapters, and after_commit callbacks do not fire)
@@ -33,7 +43,7 @@ module RailsMcpServer
33
43
  DESC
34
44
 
35
45
  arguments do
36
- required(:code).filled(:string).description("Ruby code to execute (read-only operations only)")
46
+ required(:code).filled(:string).description("Ruby code to execute (inspection operations only)")
37
47
  optional(:timeout).filled(:integer).description("Timeout in seconds. Default: 30, Max: 60")
38
48
  optional(:confirm_risky).filled(:bool).description("Set true ONLY after the user has explicitly approved running code that uses sandbox-bypass-capable constructs (send, public_send, const_get, Kernel#open). When false/absent, such code is not executed; the tool returns a CONFIRMATION REQUIRED message instead.")
39
49
  end
@@ -61,6 +71,22 @@ module RailsMcpServer
61
71
  /Process\.(spawn|exec|fork)/i,
62
72
  /Shellwords/i,
63
73
 
74
+ # Pseudo-terminals and native/syscall bridges. PTY.spawn / PTY.getpty
75
+ # start a child process outside the Kernel#system guard; Fiddle and FFI
76
+ # can call libc (e.g. system(3), execve(2)) directly. None of these are
77
+ # needed for read-only inspection.
78
+ /\bPTY\b/,
79
+ /\bFiddle\b/,
80
+ /\bFFI\b/,
81
+
82
+ # Dynamic dispatch aimed at an execution/eval sink *by name* is hard
83
+ # blocked. The general send/public_send/const_get forms stay in the
84
+ # confirmation tier below; only a dangerous literal target is rejected
85
+ # outright, so `record.send(:name)` still works while
86
+ # `Process.send(:spawn, ...)` or `const_get("Open3")` do not.
87
+ /\b(?:public_send|__send__|send)\s*(?:\(\s*)?[:'"](?:system|exec|spawn|fork|eval|popen|syscall|`)/i,
88
+ /\bconst_get\s*(?:\(\s*)?['"](?:Open3|Process|PTY|Kernel|Socket|Fiddle|FFI|Binding|ObjectSpace|TCPSocket|UDPSocket)\b/i,
89
+
64
90
  # Network access
65
91
  /Net::(HTTP|FTP|SMTP)/i,
66
92
  /URI\.(open|parse)/i,
@@ -91,11 +117,31 @@ module RailsMcpServer
91
117
  /Rails\.application\.credentials/i,
92
118
  /Rails\.application\.secrets/i,
93
119
 
94
- # Load/require that could execute arbitrary code
95
- /load\s*[(\s]+[^)]*\$/i,
96
- /require\s+[^'"]/i
120
+ # Load/require. Under `bin/rails runner` Rails, the app's models/gems, and
121
+ # the stdlib Rails loads on boot are already available, so inspection code
122
+ # almost never needs `require`. Dynamic requires and require_relative
123
+ # (loads/executes arbitrary project files) are refused outright; literal
124
+ # `require "lib"` is refused unless the lib is on REQUIRE_ALLOWLIST. This
125
+ # keeps dangerous stdlib escapes (`pty`, `open3`, `fiddle`, `ffi`,
126
+ # `socket`) out while still allowing the few pure-data libs that aren't
127
+ # always preloaded (e.g. csv, the timezone libs).
128
+ /\brequire_relative\b/i,
129
+ /require\s+[^'"]/i,
130
+ /load\s*[(\s]+[^)]*\$/i
97
131
  ].freeze
98
132
 
133
+ # The only libraries a literal `require` may name. All are pure-Ruby, with
134
+ # no process/network/native-call surface: csv (not always preloaded) and
135
+ # the timezone libs Rails uses when code touches Time.zone. Everything else
136
+ # — notably any process/native bridge — is rejected. Matched
137
+ # case-insensitively; a trailing ".rb" is ignored.
138
+ REQUIRE_ALLOWLIST = %w[csv tzinfo date time].freeze
139
+
140
+ # Extracts a literal require target from `require "x"`, `require'x'`, or
141
+ # `require("x")`. Dynamic (non-literal) requires are already rejected by the
142
+ # /require\s+[^'"]/ pattern above.
143
+ REQUIRE_STATEMENT = /\brequire\b\s*(?:\(\s*)?(['"])([^'"]+)\1/
144
+
99
145
  # Dual-use constructs that are NOT hard-blocked (they have legitimate
100
146
  # read-only uses) but can defeat the static safety scan, so running them
101
147
  # requires explicit user confirmation via confirm_risky: true.
@@ -182,7 +228,22 @@ module RailsMcpServer
182
228
  FORBIDDEN_PATTERNS.each do |pattern|
183
229
  if code.match?(pattern)
184
230
  return "REJECTED: Code contains forbidden pattern (#{pattern.source.split("\\").first}...). " \
185
- "This tool only allows read-only operations."
231
+ "This tool only allows a restricted set of inspection operations."
232
+ end
233
+ end
234
+
235
+ validate_requires(code)
236
+ end
237
+
238
+ # Rejects any literal `require` of a library outside REQUIRE_ALLOWLIST.
239
+ # (require_relative and dynamic requires are already rejected by the
240
+ # forbidden patterns.) Returns an error string, or nil when permitted.
241
+ def validate_requires(code)
242
+ code.scan(REQUIRE_STATEMENT).each do |_quote, lib|
243
+ normalized = lib.downcase.sub(/\.rb\z/, "")
244
+ unless REQUIRE_ALLOWLIST.include?(normalized)
245
+ return "REJECTED: require of '#{lib}' is not permitted. " \
246
+ "Only these libraries may be required: #{REQUIRE_ALLOWLIST.join(", ")}."
186
247
  end
187
248
  end
188
249
  nil
@@ -1,3 +1,3 @@
1
1
  module RailsMcpServer
2
- VERSION = "1.6.0"
2
+ VERSION = "1.6.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-mcp-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Alberto Chávez Cárdenas