Skip to content

Commit

Permalink
Reject/ignore untrusted command requests (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
hopsoft authored Jun 12, 2024
1 parent 7722281 commit 7e8d9cc
Show file tree
Hide file tree
Showing 20 changed files with 337 additions and 184 deletions.
17 changes: 0 additions & 17 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,3 @@ jobs:

- name: Run Tests
run: bundle exec rails test:all

# - name: Save AppMaps
# uses: actions/cache/save@v3
# if: always()
# with:
# path: ./tmp/appmap
# key: appmaps-${{ github.sha }}-${{ github.run_attempt }}

#appmap-analysis:
# if: always()
# needs: [ruby_test]
# uses: getappmap/analyze-action/.github/workflows/appmap-analysis.yml@v1
# permissions:
# actions: read
# contents: read
# checks: write
# pull-requests: write
10 changes: 2 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
*.metafile.json
.byebug_history
.containers.yml
.pnp.*
.playwright*
.pnp.*
.yarn*
/.bundle/
/doc/
Expand All @@ -18,13 +18,7 @@
/test/dummy/storage/
/test/dummy/tmp/
/tmp/
/vendor
Gemfile.lock
test/dummy/app/javascript/@turbo-boost
test/dummy/log/*.log*


# AppMap artifacts
/.appmap

# Vendored Ruby gems
/vendor
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ruby:3.0.3-slim-bullseye
FROM ruby:3.3.2-slim-bullseye

RUN apt-get -y update && \
apt-get -y --no-install-recommends install \
Expand All @@ -16,6 +16,7 @@ RUN apt-get -y --no-install-recommends install nodejs
RUN apt-get clean
RUN gem update --system
RUN bundle config set --local clean 'true'
RUN bundle config set --local gems.force true

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

Expand Down
2 changes: 0 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Specify your gem's dependencies in turbo_boost-commands.gemspec.
gemspec

gem "appmap", groups: [:development, :test]

gem "dockerfile-rails", ">= 1.3", group: :development
6 changes: 0 additions & 6 deletions appmap.yml

This file was deleted.

3 changes: 0 additions & 3 deletions bin/test

This file was deleted.

104 changes: 83 additions & 21 deletions lib/turbo_boost/commands/middlewares/entry_middleware.rb
Original file line number Diff line number Diff line change
@@ -1,31 +1,84 @@
# frozen_string_literal: true

require "device_detector"

class TurboBoost::Commands::EntryMiddleware
PATH = "/turbo-boost-command-invocation"
PARAM = "turbo_boost_command"

def initialize(app)
@app = app
end

def call(env)
request = Rack::Request.new(env)
modify! request if modify?(request)

# a command was not requested, pass through and exit early
return @app.call(env) unless command_request?(request)

# a command was requested
return [403, {"Content-Type" => "text/plain"}, ["Forbidden"]] if untrusted_client?(request)
modify_request!(request) if modify_request?(request)
@app.call env
end

private

# Returns the MIME type for TurboBoost Command invocations.
def mime_type
Mime::Type.lookup_by_extension(:turbo_boost)
@mime_type ||= Mime::Type.lookup_by_extension(:turbo_boost)
end

# Indicates if the client's user agent is trusted (i.e. known and not a bot)
#
# @param request [Rack::Request] the request to check
# @return [Boolean]
def trusted_client?(request)
client = DeviceDetector.new(request.env["HTTP_USER_AGENT"])
return false unless client.known?
return false if client.bot?
true
rescue => error
puts "#{self.class.name} failed to determine if the client is valid! #{error.message}"
false
end

# Indicates if the client's user agent is untrusted (i.e. unknown or a bot)
#
# @param request [Rack::Request] the request to check
# @return [Boolean]
def untrusted_client?(request)
!trusted_client?(request)
end

# Indicates if the request is invoking a TurboBoost Command.
#
# @param request [Rack::Request] the request to check
# @return [Boolean]
def command_request?(request)
return false unless request.post?
return false unless request.path.start_with?(PATH) || request.params.key?(PARAM)
true
end

# The TurboBoost Command params.
#
# @param request [Rack::Request] the request to extract the params from
# @return [Hash]
def command_params(request)
return {} unless command_request?(request)
return request.params[PARAM] if request.params.key?(PARAM)
JSON.parse(request.body.string)
end

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

def convert_to_get_request?(driver)
return true if driver == "frame" || driver == "window"
false
end

# Modifies the given POST request so Rails sees it as GET.
#
# The posted JSON body content holds the TurboBoost Command meta data.
Expand All @@ -65,28 +113,42 @@ def convert_to_get_request?(driver)
# }
#
# @param request [Rack::Request] the request to modify
def modify!(request)
params = JSON.parse(request.body.string)
def modify_request!(request)
params = command_params(request)
uri = URI.parse(params["src"])

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

# Update the URI, PATH_INFO, and QUERY_STRING
# Change URI and path
env["REQUEST_URI"] = uri.to_s if env.key?("REQUEST_URI")
env["PATH_INFO"] = uri.path
env["REQUEST_PATH"] = uri.path
env["PATH_INFO"] = begin
script_name = Rails.application.config.relative_url_root
path_info = uri.path.sub(/^#{Regexp.escape(script_name.to_s)}/, "")
path_info.empty? ? "/" : path_info
end

# Change query string
env["QUERY_STRING"] = uri.query.to_s
env.delete("rack.request.query_hash")

# Change the method from POST to GET
if convert_to_get_request?(params["driver"])
env["REQUEST_METHOD"] = "GET"
# Clear form data
env.delete("rack.request.form_input")
env.delete("rack.request.form_hash")
env.delete("rack.request.form_vars")
env.delete("rack.request.form_pairs")

# Clear the body and related headers so the appears and behaves like a GET
env["rack.input"] = StringIO.new
env["CONTENT_LENGTH"] = "0"
env.delete("CONTENT_TYPE")
end
# Clear the body so we can change the the method to GET
env["rack.input"] = StringIO.new
env["CONTENT_LENGTH"] = "0"
env["content-length"] = "0"
env.delete("CONTENT_TYPE")
env.delete("content-type")

# Change the method to GET
env["REQUEST_METHOD"] = "GET"
end
rescue => error
puts "#{self.class.name} failed to modify the request! #{error.message}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module TurboBoost::Commands::Patches::ActionViewHelpersTagHelperTagBuilderPatch
def tag_options(options, ...)
options = turbo_boost&.state&.tag_options(options) || options
options = TurboBoost::Commands::AttributeHydration.dehydrate(options)
super(options, ...)
super
end

private
Expand Down
5 changes: 2 additions & 3 deletions lib/turbo_boost/commands/token_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ def validate!
def tokens
list = Set.new.tap do |set|
set.add command.params[:csrf_token]

# TODO: Update to use Rails' public API
set.merge controller.send(:request_authenticity_tokens)
set.add controller.request.x_csrf_token
set.add controller.params[controller.class.request_forgery_protection_token]
end

list.select(&:present?).to_a
Expand Down
Loading

0 comments on commit 7e8d9cc

Please sign in to comment.