Skip to content

Commit

Permalink
Merge pull request #28 from basecamp/protect-dynamic-extensions
Browse files Browse the repository at this point in the history
Protect dynamic extensions
  • Loading branch information
jorgemanrubia authored Sep 6, 2021
2 parents 9da1738 + dbec605 commit 18e96a9
Show file tree
Hide file tree
Showing 21 changed files with 123 additions and 57 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Performance:
Style/FrozenStringLiteralComment:
Enabled: false

Lint/UselessAssignment:
Enabled: false

Style/StringLiterals:
Enabled: true
EnforcedStyle: double_quotes
Expand Down
25 changes: 13 additions & 12 deletions config/protections.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
static_validations:
validations:
forbidden_reopening:
- ActiveRecord
- Console1984
- PG
- Mysql2
- IRB
forbidden_constant_reference:
always:
- Console1984
- IRB
protected:
- PG
- Mysql2
Expand All @@ -16,15 +18,14 @@ static_validations:
- Console1984
- secret
- credentials
- irb
forbidden_methods:
always:
user:
Kernel:
- eval
Object:
- eval
BasicObject:
- eval
- instance_eval
Module:
- class_eval
Kernel:
- eval
Object:
- eval
BasicObject:
- eval
- instance_eval
Module:
- class_eval
18 changes: 15 additions & 3 deletions lib/console1984/command_executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ def execute(commands, &block)
run_as_system { session_logger.before_executing commands }
validate_command commands
execute_in_protected_mode(&block)
rescue Console1984::Errors::ForbiddenCommand, FrozenError
rescue Console1984::Errors::ForbiddenCommandAttempted, FrozenError
flag_suspicious(commands)
rescue Console1984::Errors::SuspiciousCommand
rescue Console1984::Errors::SuspiciousCommandAttempted
flag_suspicious(commands)
execute_in_protected_mode(&block)
rescue Console1984::Errors::ForbiddenCommandExecuted
# We detected that a forbidden command was executed. We exit IRB right away.
flag_suspicious(commands)
Console1984.supervisor.exit_irb
ensure
run_as_system { session_logger.after_executing commands }
end
Expand Down Expand Up @@ -65,13 +69,21 @@ def validate_command(command)
command_validator.validate(command)
end

def from_irb?(backtrace)
executing_user_command? && backtrace.find do |line|
line_from_irb = line =~ /^[^\/]/
break if !(line =~ /console1984\/lib/ || line_from_irb)
line_from_irb
end
end

private
def command_validator
@command_validator ||= build_command_validator
end

def build_command_validator
Console1984::CommandValidator.from_config(Console1984.protections_config.static_validations)
Console1984::CommandValidator.from_config(Console1984.protections_config.validations)
end

def flag_suspicious(commands)
Expand Down
2 changes: 1 addition & 1 deletion lib/console1984/command_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#
# The validation itself happens as a chain of validation objects. The system will invoke
# each validation in order. Validations will raise an error if the validation fails (typically
# a Console1984::Errors::ForbiddenCommand or Console1984::Errors::SuspiciousCommands).
# a Console1984::Errors::ForbiddenCommandAttempted or Console1984::Errors::SuspiciousCommands).
#
# Internally, validations will receive a Console1984::CommandValidator::ParsedCommand object. This
# exposes parsed constructs in addition to the raw strings so that validations can use those.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ def initialize(shield = Console1984.shield, config)
@constant_names_forbidden_in_protected_mode = config[:protected] || []
end

# Raises a Console1984::Errors::ForbiddenCommand if a banned constant is referenced.
# Raises a Console1984::Errors::ForbiddenCommandAttempted if a banned constant is referenced.
def validate(parsed_command)
if contains_invalid_const_reference?(parsed_command, @forbidden_constants_names) ||
(@shield.protected_mode? && contains_invalid_const_reference?(parsed_command, @constant_names_forbidden_in_protected_mode))
raise Console1984::Errors::ForbiddenCommand
raise Console1984::Errors::ForbiddenCommandAttempted
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ def initialize(banned_classes_or_modules)
@banned_class_or_module_names = banned_classes_or_modules.collect(&:to_s)
end

# Raises a Console1984::Errors::ForbiddenCommand if an banned class or module reopening
# Raises a Console1984::Errors::ForbiddenCommandAttempted if an banned class or module reopening
# is detected.
def validate(parsed_command)
if contains_invalid_class_or_module_declaration?(parsed_command)
raise Console1984::Errors::ForbiddenCommand
raise Console1984::Errors::ForbiddenCommandAttempted
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/console1984/command_validator/parsed_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def on_const(node)

def on_casgn(node)
super
scope_node, name, value_node = *node
_scope_node, name, value_node = *node
@constant_assignments.push(*extract_constants(value_node))
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def initialize(suspicious_terms)
# Raises a Console1984::Errors::SuspiciousCommand if the term is referenced.
def validate(parsed_command)
if contains_suspicious_term?(parsed_command)
raise Console1984::Errors::SuspiciousCommand
raise Console1984::Errors::SuspiciousCommandAttempted
end
end

Expand Down
8 changes: 6 additions & 2 deletions lib/console1984/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ def initialize(details)

# Attempt to execute a command that is not allowed. The system won't
# execute such commands and will flag them as sensitive.
class ForbiddenCommand < StandardError; end
class ForbiddenCommandAttempted < StandardError; end

# A suspicious command was executed. The command will be flagged but the system
# will let it run.
class SuspiciousCommand < StandardError; end
class SuspiciousCommandAttempted < StandardError; end

# A forbidden command was executed. The system will flag the command
# and exit.
class ForbiddenCommandExecuted < StandardError; end

# Attempt to incinerate a session ahead of time as determined by
# +config.console1984.incinerate_after+.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Console1984::Ext::ActiveRecord::ProtectedAuditableTables
define_method method do |*args, **kwargs|
sql = args.first
if Console1984.command_executor.executing_user_command? && sql =~ auditable_tables_regexp
raise Console1984::Errors::ForbiddenCommand, "#{sql}"
raise Console1984::Errors::ForbiddenCommandAttempted, "#{sql}"
else
super(*args, **kwargs)
end
Expand Down
19 changes: 18 additions & 1 deletion lib/console1984/ext/core/module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,26 @@ module Console1984::Ext::Core::Module

def instance_eval(*)
if Console1984.command_executor.executing_user_command?
raise Console1984::Errors::ForbiddenCommand
raise Console1984::Errors::ForbiddenCommandAttempted
else
super
end
end

def method_added(method)
if Console1984.command_executor.from_irb?(caller) && banned_for_reopening?
raise Console1984::Errors::ForbiddenCommandExecuted, "Trying to add method `#{method}` to #{self.name}"
end
end

private
def banned_for_reopening?
classes_and_modules_banned_for_reopening.find do |banned_class_or_module_name|
"#{self.name}::".start_with?("#{banned_class_or_module_name}::")
end
end

def classes_and_modules_banned_for_reopening
@classes_and_modules_banned_for_reopening ||= Console1984.protections_config.validations[:forbidden_reopening]
end
end
2 changes: 1 addition & 1 deletion lib/console1984/ext/core/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def const_get(*arguments)
# See the list +forbidden_reopening+ in +config/command_protections.yml+.
Console1984.command_executor.validate_command("class #{arguments.first}; end")
super
rescue Console1984::Errors::ForbiddenCommand
rescue Console1984::Errors::ForbiddenCommandAttempted
raise
rescue StandardError
super
Expand Down
2 changes: 1 addition & 1 deletion lib/console1984/freezeable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def prevent_instance_data_manipulation
private
def prevent_sensitive_method(method_name)
define_method method_name do |*arguments|
raise Console1984::Errors::ForbiddenCommand, "You can't invoke #{method_name} on #{self}"
raise Console1984::Errors::ForbiddenCommandAttempted, "You can't invoke #{method_name} on #{self}"
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/console1984/protections_config.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
class Console1984::ProtectionsConfig
include Console1984::Freezeable

delegate :static_validations, to: :instance
delegate :validations, to: :instance

attr_reader :config

def initialize(config)
@config = config
end

%i[ static_validations forbidden_methods ].each do |method_name|
%i[ validations forbidden_methods ].each do |method_name|
define_method method_name do
config[method_name].symbolize_keys
end
Expand Down
18 changes: 6 additions & 12 deletions lib/console1984/shield/method_invocation_shell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@ class Console1984::Shield::MethodInvocationShell
include Console1984::Freezeable

class << self
def install_for(config)
Array(config[:user]).each { |invocation| self.new(invocation, only_for_user_commands: true).prevent_methods_invocation }
Array(config[:system]).each { |invocation| self.new(invocation, only_for_user_commands: false).prevent_methods_invocation }
def install_for(invocations)
Array(invocations).each { |invocation| self.new(invocation).prevent_methods_invocation }
end
end

attr_reader :class_name, :methods, :only_for_user_commands

def initialize(invocation, only_for_user_commands:)
def initialize(invocation)
@class_name, methods = invocation.to_a
@methods = Array(methods)
@only_for_user_commands = only_for_user_commands
end

def prevent_methods_invocation
class_name.constantize.prepend build_protection_module
class_name.to_s.constantize.prepend build_protection_module
end

def build_protection_module
Expand All @@ -37,12 +35,8 @@ def protected_method_invocations_source
def protected_method_invocation_source_for(method)
<<~RUBY
def #{method}(*args)
if (!#{only_for_user_commands} || Console1984.command_executor.executing_user_command?) && caller.find do |line|
line_from_irb = line =~ /^[^\\/]/
break if !(line =~ /console1984\\/lib/ || line_from_irb)
line_from_irb
end
raise Console1984::Errors::ForbiddenCommand
if Console1984.command_executor.from_irb?(caller)
raise Console1984::Errors::ForbiddenCommandAttempted
else
super
end
Expand Down
5 changes: 5 additions & 0 deletions lib/console1984/supervisor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ def stop
stop_session
end

def exit_irb
stop
IRB.CurrentContext.exit
end

private
def require_dependencies
Kernel.silence_warnings do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
require "test_helper"

class ForbiddenConstantReferenceValidationTest < ActiveSupport::TestCase
test "validate referencing constant that are always forbidden will raise a ForbiddenCommand error" do
assert_raise Console1984::Errors::ForbiddenCommand do
test "validate referencing constant that are always forbidden will raise a ForbiddenCommandAttempted error" do
assert_raise Console1984::Errors::ForbiddenCommandAttempted do
run_validation <<~RUBY, always: ["SomeClass"]
SomeClass.some_method
RUBY
end
end

test "validate referencing namespaced constants that are always forbidden will raise a ForbiddenCommand error" do
assert_raise Console1984::Errors::ForbiddenCommand do
test "validate referencing namespaced constants that are always forbidden will raise a ForbiddenCommandAttempted error" do
assert_raise Console1984::Errors::ForbiddenCommandAttempted do
run_validation <<~RUBY, always: ["Some::Base::Class"]
puts Some::Base::Class.config
RUBY
end
end

test "validate referencing a namespaced constant where the parent constant is banned" do
assert_raise Console1984::Errors::ForbiddenCommand do
assert_raise Console1984::Errors::ForbiddenCommandAttempted do
run_validation <<~RUBY, always: ["Some"]
puts Some::Base::Class.config
RUBY
end
end

test "validate constants with leading ::" do
assert_raise Console1984::Errors::ForbiddenCommand do
assert_raise Console1984::Errors::ForbiddenCommandAttempted do
run_validation <<~RUBY, always: ["Some"]
puts ::Some::Base::Class.config
RUBY
end
end

test "validate referencing constant that are forbidden in protected mode will raise a ForbiddenCommand error only in protected mode" do
test "validate referencing constant that are forbidden in protected mode will raise a ForbiddenCommandAttempted error only in protected mode" do
run_validation <<~RUBY, protected: ["SomeClass"], shield: OpenStruct.new(protected_mode?: false)
SomeClass.some_method
RUBY

assert_raise Console1984::Errors::ForbiddenCommand do
assert_raise Console1984::Errors::ForbiddenCommandAttempted do
run_validation <<~RUBY, protected: ["SomeClass"], shield: OpenStruct.new(protected_mode?: true)
SomeClass.some_method
RUBY
Expand Down
12 changes: 6 additions & 6 deletions test/command_validator/forbidden_reopening_validation_test.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
require "test_helper"

class ForbiddenReopeningValidationTest < ActiveSupport::TestCase
test "validate reopening classes that are always forbidden will raise a ForbiddenCommand error" do
assert_raise Console1984::Errors::ForbiddenCommand do
test "validate reopening classes that are always forbidden will raise a ForbiddenCommandAttempted error" do
assert_raise Console1984::Errors::ForbiddenCommandAttempted do
run_validation <<~RUBY, ["SomeClass"]
class SomeClass
end
RUBY
end
end

test "validate reopening modules that are always forbidden will raise a ForbiddenCommand error" do
assert_raise Console1984::Errors::ForbiddenCommand do
test "validate reopening modules that are always forbidden will raise a ForbiddenCommandAttempted error" do
assert_raise Console1984::Errors::ForbiddenCommandAttempted do
run_validation <<~RUBY, ["SomeModule"]
module SomeModule
end
Expand All @@ -20,7 +20,7 @@ module SomeModule
end

test "validate reopening namespaced classes" do
assert_raise Console1984::Errors::ForbiddenCommand do
assert_raise Console1984::Errors::ForbiddenCommandAttempted do
run_validation <<~RUBY, ["Some::Base::Class"]
class Some::Base::Class
end
Expand All @@ -29,7 +29,7 @@ class Some::Base::Class
end

test "validate reopening namespaced classes when the parent module is banned" do
assert_raise Console1984::Errors::ForbiddenCommand do
assert_raise Console1984::Errors::ForbiddenCommandAttempted do
run_validation <<~RUBY, ["Some"]
module Some::Base::Class
end
Expand Down
2 changes: 1 addition & 1 deletion test/command_validator/suspicious_terms_validation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

class SuspicipusTermsValidationTest < ActiveSupport::TestCase
test "raises a SuspiciousCommand error when a suspicious term appears in the command" do
assert_raise Console1984::Errors::SuspiciousCommand do
assert_raise Console1984::Errors::SuspiciousCommandAttempted do
run_validation <<~RUBY, ["woah"]
foo = "woah"
RUBY
Expand Down
Loading

0 comments on commit 18e96a9

Please sign in to comment.