slash_console 0.1.5 → 0.1.7

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: '09b6efbfc6effb15fae37a397c78d62ae8408e98d44819f64eadac99f438c0f9'
4
- data.tar.gz: 6e5f82dd8814dda0aa99f4b7d5875e406977e78f01099fe98e68684be306361b
3
+ metadata.gz: fe6fc75ab8103bf062f9a10eefdcc0385749f4c1889b04fb1c6ba0f1a99a939d
4
+ data.tar.gz: 6186eb67c87e30ebb7c0e09dca9cd4d38212d5186fd32571a0ee89aedaaea780
5
5
  SHA512:
6
- metadata.gz: 6a2714e7502c931378d6f9fdd3598ce60c8577fc159bd214bd23826f64b3218c6115d2294a5ac885a7a4d4f3c17fee0e09aa07c241210bd7379ea1af78ba1b7b
7
- data.tar.gz: e0219858fd1e1b7f768a71d0d35d9fe2b711804b30e4f9a5297fafd007990986a84194b6007e6aca84b7fb9980c09bbde7b943d46c981986af5ca32487f59619
6
+ metadata.gz: 611af64c4b066ec322875c66d9eac0a50ff0cc0102344240b24f1dd501ec60cd2241f3d19cd3f45fd593c0c63205685483bcdf79df23b18081e91e141618432d
7
+ data.tar.gz: 9f7f467142f12e16668899c9cb150e7b7165be1f2df1b099d93d2809bc2d5fb0ffb36f32d2befdd752be3c83cb2850331ad91bae944510704b76379c8400d6d1
data/CHANGELOG.md CHANGED
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.7] - 2026-07-10
11
+
12
+ ### Security
13
+ - Authentication is now required in every environment except `development`
14
+ and `test`, instead of only `production`. The engine force-enables
15
+ web-console and allows all IPs everywhere, so a `staging`/`preview`
16
+ deployment previously served an unauthenticated console.
17
+ - Require web-console >= 4.2.1: versions before 4.1.0 do not put CSP
18
+ nonces on injected assets (silently breaking under a strict CSP), and
19
+ versions before 4.2.1 lack Rack 3 / Rails 7.1 support.
20
+ - Enabled `rubygems_mfa_required`, so future gem pushes and yanks require
21
+ a multi-factor-authenticated RubyGems session.
22
+
23
+ ### Fixed
24
+ - A malformed Basic Authorization header (credentials without a colon)
25
+ now gets a 401 on Rack 2 instead of raising.
26
+ - Stored console sessions are capped at 50 per process. web-console's
27
+ session store is never evicted otherwise, so each console page load
28
+ pinned a binding in memory until process restart.
29
+
30
+ ### Removed
31
+ - Deleted the engine's unused application layout, which referenced a
32
+ stylesheet that does not ship with the gem, and a no-op CSRF skip in
33
+ the console controller (the console page is GET-only).
34
+
35
+ ## [0.1.6] - 2026-07-10
36
+
37
+ ### Security
38
+ - Production Basic authentication moved from the controller into a Rack
39
+ middleware inserted in front of `WebConsole::Middleware`, so it now covers
40
+ web-console's evaluator endpoints (`/__web_console/repl_sessions/:id`) as
41
+ well as the console page. Previously a console session ID acted as an
42
+ unauthenticated bearer token for code execution until process restart.
43
+
44
+ ### Fixed
45
+ - The console renders under a strict nonce-based Content Security Policy.
46
+ The page stylesheet carries Rails' per-request nonce, and requesting the
47
+ nonce during rendering makes it available to web-console's injected
48
+ scripts even when the app hoists the CSP middleware above web-console
49
+ (previously such apps got a nonempty header nonce but `nonce=""` scripts,
50
+ i.e. a blank console).
51
+ - Console input evaluates in a fresh top-level binding, so constants
52
+ resolve the same way as in `bin/rails console` instead of inside the
53
+ engine's namespace (`ApplicationController` no longer resolves to
54
+ `SlashConsole::ApplicationController`, and app constants no longer need a
55
+ leading `::`). Local variables persist within a console session but not
56
+ across sessions.
57
+ - Removed the duplicate `WebConsole::Middleware` the engine inserted
58
+ alongside the copy web-console's own railtie already adds.
59
+
10
60
  ## [0.1.5] - 2025-09-15
11
61
 
12
62
  ### Fixed
@@ -53,5 +103,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
53
103
  - Development mode with no authentication required
54
104
  - Standard Ruby style guide compliance
55
105
 
56
- [Unreleased]: https://github.com/firstdraft/slash_console/compare/v0.1.0...HEAD
57
- [0.1.0]: https://github.com/firstdraft/slash_console/releases/tag/v0.1.0
106
+ [Unreleased]: https://github.com/firstdraft/slash_console/compare/v0.1.7...HEAD
107
+ [0.1.7]: https://github.com/firstdraft/slash_console/compare/v0.1.6...v0.1.7
108
+ [0.1.6]: https://github.com/firstdraft/slash_console/compare/v0.1.0...v0.1.6
109
+ [0.1.0]: https://github.com/firstdraft/slash_console/releases/tag/v0.1.0
data/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  A Rails engine that provides a web-based console interface at `/rails/console`, allowing easy access to a Rails console in both development and production environments.
4
4
 
5
+ ⚠️ **Security Warning**: This gem provides direct access to the `rails console`. Anyone who accesses it can run arbitrary code on the server: stealing/deleting all your data, planting bugs, sending scam emails, mining cryptocurrency, etc. So, be careful.
6
+
7
+ - Only use this gem in applications where the security trade-offs are acceptable. Basically, only for toy apps/proofs-of-concept/portfolio projects that contain only sample data. Never use this gem when real user data is at risk.
8
+ - For serious apps, SSH into the server and run `rails console` at the command-line. This may require upgrading your hosting from free to paid, but you should be doing that anyway if you have real users.
9
+ - Make up a strong, unique `ADMIN_PASSWORD` for each app. There is no rate limiting on the password prompt, so a short or guessable password can be brute-forced.
10
+
5
11
  ## Installation
6
12
 
7
13
  Add this line to your application's Gemfile:
@@ -10,7 +16,7 @@ Add this line to your application's Gemfile:
10
16
  gem "slash_console"
11
17
  ```
12
18
 
13
- And then execute:
19
+ And then:
14
20
 
15
21
  ```bash
16
22
  bundle install
@@ -22,15 +28,11 @@ That's it! Navigate to `/rails/console` in your browser.
22
28
 
23
29
  ### Development
24
30
 
25
- In development, no authentication is required. Simply visit:
26
-
27
- ```
28
- http://localhost:3000/rails/console
29
- ```
31
+ In development, no authentication is required. Simply visit `/rails/console`.
30
32
 
31
- ### Production
33
+ ### Deployed environments
32
34
 
33
- For production use, authentication is **required**. Set these environment variables:
35
+ In every environment other than `development` and `test` — production, staging, previews, anything — authentication is required. Set these environment variables:
34
36
 
35
37
  ```bash
36
38
  ADMIN_USERNAME="choose_your_own_username"
@@ -39,30 +41,33 @@ ADMIN_PASSWORD="choose_your_own_strong_password"
39
41
 
40
42
  Without these environment variables, you'll see an error message explaining what needs to be configured.
41
43
 
42
- ⚠️ **Security Warning**: This gem provides direct access to your Rails console. In production:
43
-
44
- - Only use for applications where the security trade-offs are acceptable. Basically, only for toy apps; never where real user data is at risk. For serious apps, SSH into the server and run `rails console` at the command-line.
45
- - Make up a strong, unique `ADMIN_PASSWORD` for each app.
46
- - In production, the gem allows console access from all IP addresses (0.0.0.0/0) when authenticated. The authentication layer is your only protection.
44
+ If your server runs multiple processes (e.g. Puma workers), console sessions live in the memory of whichever process rendered the page, so evaluating code may intermittently report that your session is no longer available. Run a single process (e.g. `WEB_CONCURRENCY=1`) for a reliable console.
47
45
 
48
46
  ## How It Works
49
47
 
50
- SlashConsole is a lightweight wrapper around the excellent `web-console` gem. It:
48
+ SlashConsole is a lightweight wrapper around [the excellent `web-console` gem](https://github.com/rails/web-console). It:
51
49
 
52
50
  1. Provides a dedicated route for console access (instead of only on error pages).
53
- 2. Renders a full-page console interface.
54
- 3. Uses basic authentication when in production.
51
+ 2. Renders a full-page console interface, including in apps that enforce a strict nonce-based Content Security Policy.
52
+ 3. In every environment except development and test, requires basic authentication via a Rack middleware that protects both the console page and web-console's code-evaluation endpoints (`/__web_console/repl_sessions/:id`).
53
+ 4. Evaluates console input at the top level, so constants resolve the same way as in `bin/rails console`.
54
+
55
+ Note that SlashConsole configures web-console itself: it activates it in all environments and clears its IP allowlist (authentication replaces it). Any `config.web_console` settings in your app will be overridden.
55
56
 
56
57
  ## Development
57
58
 
58
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
59
+ After checking out the repo, run `bundle install` to install dependencies.
59
60
 
60
61
  To install this gem onto your local machine, run `bundle exec rake install`.
61
62
 
62
63
  ### Running Tests
63
64
 
65
+ The test suite needs a local PostgreSQL server. Prepare the test database once, then:
66
+
64
67
  ```bash
65
- bundle exec rake test
68
+ bundle exec rake app:db:test:prepare
69
+ bundle exec rake test # integration + unit tests
70
+ bundle exec rake test:system # browser smoke tests (needs Chrome)
66
71
  ```
67
72
 
68
73
  ### Linting
data/Rakefile CHANGED
@@ -6,3 +6,19 @@ load "rails/tasks/engine.rake"
6
6
  load "rails/tasks/statistics.rake"
7
7
 
8
8
  require "bundler/gem_tasks"
9
+
10
+ require "rake/testtask"
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << "test"
14
+ t.test_files = FileList["test/**/*_test.rb"].exclude("test/system/**/*", "test/dummy/**/*")
15
+ t.verbose = false
16
+ end
17
+
18
+ Rake::TestTask.new("test:system") do |t|
19
+ t.libs << "test"
20
+ t.test_files = FileList["test/system/**/*_test.rb"]
21
+ t.verbose = false
22
+ end
23
+
24
+ task default: :test
@@ -2,30 +2,44 @@ module SlashConsole
2
2
  class ConsoleController < ApplicationController
3
3
  layout false
4
4
 
5
- skip_before_action :verify_authenticity_token, if: -> { defined?(verify_authenticity_token) }
5
+ # web-console stores every console session (and the binding it holds)
6
+ # in an in-memory hash that is never evicted, so each page load would
7
+ # otherwise pin memory until the process restarts. Old sessions are
8
+ # dropped once this many accumulate; an evicted session's tab shows
9
+ # web-console's normal "session is no longer available" message.
10
+ MAX_STORED_SESSIONS = 50
6
11
 
7
- before_action :ensure_credentials_configured, if: -> { Rails.env.production? }
8
- before_action :authenticate_user, if: -> { Rails.env.production? }
12
+ # BasicAuthMiddleware is the primary guard: it protects both this page
13
+ # and web-console's evaluator endpoints, but only at the engine's
14
+ # standard /rails mount point. These filters remain as a second layer
15
+ # so the page stays protected if an application mounts the engine at a
16
+ # custom path.
17
+ before_action :ensure_credentials_configured, if: -> { BasicAuthMiddleware.authentication_required? }
18
+ before_action :authenticate_user, if: -> { BasicAuthMiddleware.authentication_required? }
9
19
 
10
20
  def index
11
- console
21
+ prune_console_sessions
22
+ console(SlashConsole.console_binding)
12
23
  render :index
13
24
  end
14
25
 
15
26
  private
16
27
 
17
28
  def ensure_credentials_configured
18
- if ENV["ADMIN_USERNAME"].blank? || ENV["ADMIN_PASSWORD"].blank?
19
- render plain: 'Before you can access the console, you must set environment variables called "ADMIN_USERNAME" and "ADMIN_PASSWORD".',
20
- status: :service_unavailable
29
+ unless BasicAuthMiddleware.credentials_configured?
30
+ render plain: BasicAuthMiddleware::CREDENTIALS_MESSAGE, status: :service_unavailable
21
31
  end
22
32
  end
23
33
 
24
34
  def authenticate_user
25
- authenticate_or_request_with_http_basic("Rails Console") do |username, password|
26
- ActiveSupport::SecurityUtils.secure_compare(username, ENV["ADMIN_USERNAME"]) &&
27
- ActiveSupport::SecurityUtils.secure_compare(password, ENV["ADMIN_PASSWORD"])
35
+ authenticate_or_request_with_http_basic(BasicAuthMiddleware::REALM) do |username, password|
36
+ BasicAuthMiddleware.authorized?(username, password)
28
37
  end
29
38
  end
39
+
40
+ def prune_console_sessions
41
+ storage = WebConsole::Session.inmemory_storage
42
+ storage.shift while storage.size >= MAX_STORED_SESSIONS
43
+ end
30
44
  end
31
45
  end
@@ -3,7 +3,9 @@
3
3
  <head>
4
4
  <title>Rails Console</title>
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <style>
6
+ <%# Asking Rails for the nonce here also makes it available to the
7
+ web-console middleware when it injects the console scripts below. %>
8
+ <style nonce="<%= content_security_policy_nonce %>">
7
9
  body, html {
8
10
  margin: 0;
9
11
  padding: 0;
@@ -18,4 +20,4 @@
18
20
  <body>
19
21
  <!-- Console will be injected here by web-console middleware -->
20
22
  </body>
21
- </html>
23
+ </html>
@@ -0,0 +1,89 @@
1
+ module SlashConsole
2
+ # Basic authentication for the console in deployed environments.
3
+ #
4
+ # Authentication must happen in the middleware stack, not in
5
+ # ConsoleController: web-console's evaluator endpoints (PUT/POST
6
+ # <mount_point>/repl_sessions/:id) are served directly by
7
+ # WebConsole::Middleware and never reach the router, so a controller
8
+ # filter cannot protect them. This middleware is inserted before
9
+ # WebConsole::Middleware and guards both the console page and the
10
+ # evaluator endpoints with the same credentials.
11
+ class BasicAuthMiddleware
12
+ REALM = "Rails Console"
13
+ CREDENTIALS_MESSAGE = 'Before you can access the console, you must set environment variables called "ADMIN_USERNAME" and "ADMIN_PASSWORD".'
14
+
15
+ # The paths the engine serves at its standard mount point: the root
16
+ # (/rails), the console page, and either with a format suffix or
17
+ # trailing slash. Deliberately does not cover other /rails/* paths --
18
+ # unmatched requests cascade past the engine to the host app, which
19
+ # owns paths like /rails/active_storage/*.
20
+ CONSOLE_PATHS = %r{\A/rails(?:/(?:console)?(?:\.[^/]*)?/?)?\z}
21
+
22
+ class << self
23
+ # Development and test are the only environments that get an open
24
+ # console. Any deployed environment -- production, staging, preview,
25
+ # or anything else -- must present credentials, because the engine
26
+ # force-enables web-console everywhere and allows all IPs.
27
+ def authentication_required?
28
+ !(Rails.env.development? || Rails.env.test?)
29
+ end
30
+
31
+ def credentials_configured?
32
+ ENV["ADMIN_USERNAME"].present? && ENV["ADMIN_PASSWORD"].present?
33
+ end
34
+
35
+ def authorized?(username, password)
36
+ ActiveSupport::SecurityUtils.secure_compare(username.to_s, ENV["ADMIN_USERNAME"].to_s) &&
37
+ ActiveSupport::SecurityUtils.secure_compare(password.to_s, ENV["ADMIN_PASSWORD"].to_s)
38
+ end
39
+ end
40
+
41
+ def initialize(app)
42
+ @app = app
43
+ end
44
+
45
+ def call(env)
46
+ return @app.call(env) unless self.class.authentication_required? && protects?(env[Rack::PATH_INFO].to_s)
47
+
48
+ unless self.class.credentials_configured?
49
+ return [503, {"content-type" => "text/plain"}, [CREDENTIALS_MESSAGE]]
50
+ end
51
+
52
+ if valid_credentials?(env)
53
+ @app.call(env)
54
+ else
55
+ unauthorized
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ # Destructures rather than splatting: Rack 2 (Rails 7.0 apps) does not
62
+ # validate credential arity in Request#basic?, so a header whose
63
+ # decoded value lacks a colon would otherwise raise instead of 401ing.
64
+ def valid_credentials?(env)
65
+ auth = Rack::Auth::Basic::Request.new(env)
66
+ return false unless auth.provided? && auth.basic?
67
+
68
+ username, password = auth.credentials
69
+ self.class.authorized?(username, password)
70
+ end
71
+
72
+ def protects?(path)
73
+ CONSOLE_PATHS.match?(path) || evaluator_path?(path)
74
+ end
75
+
76
+ def evaluator_path?(path)
77
+ mount_point = WebConsole::Middleware.mount_point
78
+ path == mount_point || path.start_with?("#{mount_point}/")
79
+ end
80
+
81
+ def unauthorized
82
+ headers = {
83
+ "content-type" => "text/plain",
84
+ "www-authenticate" => %(Basic realm="#{REALM}")
85
+ }
86
+ [401, headers, ["HTTP Basic: Access denied.\n"]]
87
+ end
88
+ end
89
+ end
@@ -13,8 +13,13 @@ module SlashConsole
13
13
  require "web_console/extensions"
14
14
  end
15
15
 
16
- initializer "slash_console.insert_middleware", after: :load_config_initializers do |app|
17
- app.config.middleware.insert_after ActionDispatch::DebugExceptions, WebConsole::Middleware
16
+ # web-console's own railtie already inserts WebConsole::Middleware
17
+ # (before ActionDispatch::DebugExceptions), so the engine must not
18
+ # insert a second copy. It only adds the authentication middleware,
19
+ # which has to sit in front of WebConsole::Middleware because the
20
+ # evaluator endpoints are served there and never reach the router.
21
+ initializer "slash_console.insert_basic_auth_middleware", after: "web_console.insert_middleware" do |app|
22
+ app.config.middleware.insert_before WebConsole::Middleware, SlashConsole::BasicAuthMiddleware
18
23
  end
19
24
 
20
25
  initializer "slash_console.mount_engine" do |app|
@@ -1,3 +1,3 @@
1
1
  module SlashConsole
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.7"
3
3
  end
data/lib/slash_console.rb CHANGED
@@ -1,6 +1,15 @@
1
1
  require "slash_console/version"
2
+ require "slash_console/basic_auth_middleware"
2
3
  require "slash_console/engine"
3
4
  require "web-console"
4
5
 
5
6
  module SlashConsole
7
+ # A fresh binding at the top level, so that console input sees the same
8
+ # +self+ and constant resolution as bin/rails console, rather than the
9
+ # engine's lexical namespace. Each call returns a new binding: local
10
+ # variables persist across evaluations within a console session but do
11
+ # not leak between sessions or into TOPLEVEL_BINDING itself.
12
+ def self.console_binding
13
+ TOPLEVEL_BINDING.eval("binding")
14
+ end
6
15
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slash_console
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raghu Betina
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '4.0'
32
+ version: 4.2.1
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '4.0'
39
+ version: 4.2.1
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: standard
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -66,10 +66,10 @@ files:
66
66
  - app/assets/stylesheets/slash_console/application.css
67
67
  - app/controllers/slash_console/application_controller.rb
68
68
  - app/controllers/slash_console/console_controller.rb
69
- - app/views/layouts/slash_console/application.html.erb
70
69
  - app/views/slash_console/console/index.html.erb
71
70
  - config/routes.rb
72
71
  - lib/slash_console.rb
72
+ - lib/slash_console/basic_auth_middleware.rb
73
73
  - lib/slash_console/engine.rb
74
74
  - lib/slash_console/version.rb
75
75
  - lib/tasks/slash_console_tasks.rake
@@ -77,6 +77,7 @@ homepage: https://github.com/firstdraft/slash_console
77
77
  licenses:
78
78
  - MIT
79
79
  metadata:
80
+ rubygems_mfa_required: 'true'
80
81
  homepage_uri: https://github.com/firstdraft/slash_console
81
82
  source_code_uri: https://github.com/firstdraft/slash_console
82
83
  changelog_uri: https://github.com/firstdraft/slash_console/blob/main/CHANGELOG.md
@@ -96,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
97
  - !ruby/object:Gem::Version
97
98
  version: '0'
98
99
  requirements: []
99
- rubygems_version: 3.7.1
100
+ rubygems_version: 4.0.9
100
101
  specification_version: 4
101
102
  summary: Mountable Rails console at /rails/console
102
103
  test_files: []
@@ -1,17 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Slash console</title>
5
- <%= csrf_meta_tags %>
6
- <%= csp_meta_tag %>
7
-
8
- <%= yield :head %>
9
-
10
- <%= stylesheet_link_tag "slash_console/application", media: "all" %>
11
- </head>
12
- <body>
13
-
14
- <%= yield %>
15
-
16
- </body>
17
- </html>