Skip to content

Commit 7e8d9cc

Browse files
authored
Reject/ignore untrusted command requests (#142)
1 parent 7722281 commit 7e8d9cc

20 files changed

+337
-184
lines changed

.github/workflows/tests.yml

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,3 @@ jobs:
6969

7070
- name: Run Tests
7171
run: bundle exec rails test:all
72-
73-
# - name: Save AppMaps
74-
# uses: actions/cache/save@v3
75-
# if: always()
76-
# with:
77-
# path: ./tmp/appmap
78-
# key: appmaps-${{ github.sha }}-${{ github.run_attempt }}
79-
80-
#appmap-analysis:
81-
# if: always()
82-
# needs: [ruby_test]
83-
# uses: getappmap/analyze-action/.github/workflows/appmap-analysis.yml@v1
84-
# permissions:
85-
# actions: read
86-
# contents: read
87-
# checks: write
88-
# pull-requests: write

.gitignore

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
*.metafile.json
44
.byebug_history
55
.containers.yml
6-
.pnp.*
76
.playwright*
7+
.pnp.*
88
.yarn*
99
/.bundle/
1010
/doc/
@@ -18,13 +18,7 @@
1818
/test/dummy/storage/
1919
/test/dummy/tmp/
2020
/tmp/
21+
/vendor
2122
Gemfile.lock
2223
test/dummy/app/javascript/@turbo-boost
2324
test/dummy/log/*.log*
24-
25-
26-
# AppMap artifacts
27-
/.appmap
28-
29-
# Vendored Ruby gems
30-
/vendor

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM ruby:3.0.3-slim-bullseye
1+
FROM ruby:3.3.2-slim-bullseye
22

33
RUN apt-get -y update && \
44
apt-get -y --no-install-recommends install \
@@ -16,6 +16,7 @@ RUN apt-get -y --no-install-recommends install nodejs
1616
RUN apt-get clean
1717
RUN gem update --system
1818
RUN bundle config set --local clean 'true'
19+
RUN bundle config set --local gems.force true
1920

2021
RUN mkdir -p /mnt/external/node_modules /mnt/external/gems /mnt/external/database
2122

Gemfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,4 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
66
# Specify your gem's dependencies in turbo_boost-commands.gemspec.
77
gemspec
88

9-
gem "appmap", groups: [:development, :test]
10-
119
gem "dockerfile-rails", ">= 1.3", group: :development

appmap.yml

Lines changed: 0 additions & 6 deletions
This file was deleted.

bin/test

Lines changed: 0 additions & 3 deletions
This file was deleted.

lib/turbo_boost/commands/middlewares/entry_middleware.rb

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,84 @@
11
# frozen_string_literal: true
22

3+
require "device_detector"
4+
35
class TurboBoost::Commands::EntryMiddleware
46
PATH = "/turbo-boost-command-invocation"
7+
PARAM = "turbo_boost_command"
58

69
def initialize(app)
710
@app = app
811
end
912

1013
def call(env)
1114
request = Rack::Request.new(env)
12-
modify! request if modify?(request)
15+
16+
# a command was not requested, pass through and exit early
17+
return @app.call(env) unless command_request?(request)
18+
19+
# a command was requested
20+
return [403, {"Content-Type" => "text/plain"}, ["Forbidden"]] if untrusted_client?(request)
21+
modify_request!(request) if modify_request?(request)
1322
@app.call env
1423
end
1524

1625
private
1726

18-
# Returns the MIME type for TurboBoost Command invocations.
1927
def mime_type
20-
Mime::Type.lookup_by_extension(:turbo_boost)
28+
@mime_type ||= Mime::Type.lookup_by_extension(:turbo_boost)
29+
end
30+
31+
# Indicates if the client's user agent is trusted (i.e. known and not a bot)
32+
#
33+
# @param request [Rack::Request] the request to check
34+
# @return [Boolean]
35+
def trusted_client?(request)
36+
client = DeviceDetector.new(request.env["HTTP_USER_AGENT"])
37+
return false unless client.known?
38+
return false if client.bot?
39+
true
40+
rescue => error
41+
puts "#{self.class.name} failed to determine if the client is valid! #{error.message}"
42+
false
43+
end
44+
45+
# Indicates if the client's user agent is untrusted (i.e. unknown or a bot)
46+
#
47+
# @param request [Rack::Request] the request to check
48+
# @return [Boolean]
49+
def untrusted_client?(request)
50+
!trusted_client?(request)
51+
end
52+
53+
# Indicates if the request is invoking a TurboBoost Command.
54+
#
55+
# @param request [Rack::Request] the request to check
56+
# @return [Boolean]
57+
def command_request?(request)
58+
return false unless request.post?
59+
return false unless request.path.start_with?(PATH) || request.params.key?(PARAM)
60+
true
61+
end
62+
63+
# The TurboBoost Command params.
64+
#
65+
# @param request [Rack::Request] the request to extract the params from
66+
# @return [Hash]
67+
def command_params(request)
68+
return {} unless command_request?(request)
69+
return request.params[PARAM] if request.params.key?(PARAM)
70+
JSON.parse(request.body.string)
2171
end
2272

2373
# Indicates whether or not the request is a TurboBoost Command invocation that requires modifications
2474
# before we hand things over to Rails.
2575
#
76+
# @note The form and method drivers DO NOT modify the request;
77+
# instead, they let Rails mechanics handle the request as normal.
78+
#
2679
# @param request [Rack::Request] the request to check
2780
# @return [Boolean] true if the request is a TurboBoost Command invocation, false otherwise
28-
def modify?(request)
81+
def modify_request?(request)
2982
return false unless request.post?
3083
return false unless request.path.start_with?(PATH)
3184
return false unless mime_type && request.env["HTTP_ACCEPT"]&.include?(mime_type)
@@ -35,11 +88,6 @@ def modify?(request)
3588
false
3689
end
3790

38-
def convert_to_get_request?(driver)
39-
return true if driver == "frame" || driver == "window"
40-
false
41-
end
42-
4391
# Modifies the given POST request so Rails sees it as GET.
4492
#
4593
# The posted JSON body content holds the TurboBoost Command meta data.
@@ -65,28 +113,42 @@ def convert_to_get_request?(driver)
65113
# }
66114
#
67115
# @param request [Rack::Request] the request to modify
68-
def modify!(request)
69-
params = JSON.parse(request.body.string)
116+
def modify_request!(request)
117+
params = command_params(request)
70118
uri = URI.parse(params["src"])
71119

72120
request.env.tap do |env|
73121
# Store the command params in the environment
74122
env["turbo_boost_command_params"] = params
75123

76-
# Update the URI, PATH_INFO, and QUERY_STRING
124+
# Change URI and path
77125
env["REQUEST_URI"] = uri.to_s if env.key?("REQUEST_URI")
78-
env["PATH_INFO"] = uri.path
126+
env["REQUEST_PATH"] = uri.path
127+
env["PATH_INFO"] = begin
128+
script_name = Rails.application.config.relative_url_root
129+
path_info = uri.path.sub(/^#{Regexp.escape(script_name.to_s)}/, "")
130+
path_info.empty? ? "/" : path_info
131+
end
132+
133+
# Change query string
79134
env["QUERY_STRING"] = uri.query.to_s
135+
env.delete("rack.request.query_hash")
80136

81-
# Change the method from POST to GET
82-
if convert_to_get_request?(params["driver"])
83-
env["REQUEST_METHOD"] = "GET"
137+
# Clear form data
138+
env.delete("rack.request.form_input")
139+
env.delete("rack.request.form_hash")
140+
env.delete("rack.request.form_vars")
141+
env.delete("rack.request.form_pairs")
84142

85-
# Clear the body and related headers so the appears and behaves like a GET
86-
env["rack.input"] = StringIO.new
87-
env["CONTENT_LENGTH"] = "0"
88-
env.delete("CONTENT_TYPE")
89-
end
143+
# Clear the body so we can change the the method to GET
144+
env["rack.input"] = StringIO.new
145+
env["CONTENT_LENGTH"] = "0"
146+
env["content-length"] = "0"
147+
env.delete("CONTENT_TYPE")
148+
env.delete("content-type")
149+
150+
# Change the method to GET
151+
env["REQUEST_METHOD"] = "GET"
90152
end
91153
rescue => error
92154
puts "#{self.class.name} failed to modify the request! #{error.message}"

lib/turbo_boost/commands/patches/action_view_helpers_tag_helper_tag_builder_patch.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module TurboBoost::Commands::Patches::ActionViewHelpersTagHelperTagBuilderPatch
66
def tag_options(options, ...)
77
options = turbo_boost&.state&.tag_options(options) || options
88
options = TurboBoost::Commands::AttributeHydration.dehydrate(options)
9-
super(options, ...)
9+
super
1010
end
1111

1212
private

lib/turbo_boost/commands/token_validator.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,8 @@ def validate!
3636
def tokens
3737
list = Set.new.tap do |set|
3838
set.add command.params[:csrf_token]
39-
40-
# TODO: Update to use Rails' public API
41-
set.merge controller.send(:request_authenticity_tokens)
39+
set.add controller.request.x_csrf_token
40+
set.add controller.params[controller.class.request_forgery_protection_token]
4241
end
4342

4443
list.select(&:present?).to_a

0 commit comments

Comments
 (0)