-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #26 from basecamp/protect-db-drivers
Improve protection mechanisms, design and documentation
- Loading branch information
Showing
47 changed files
with
940 additions
and
324 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
forbidden_reopening: | ||
- ActiveRecord | ||
- Console1984 | ||
- PG | ||
- Mysql2 | ||
forbidden_constant_reference: | ||
always: | ||
- Console1984 | ||
protected: | ||
- PG | ||
- Mysql2 | ||
- ActiveRecord::ActiveRecordEncryption | ||
suspicious_terms: | ||
- console_1984 | ||
- Console1984 | ||
- secret | ||
- credentials |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
# Supervise execution of console commands: | ||
# | ||
# * It will {validate commands}[rdoc-ref:Console1984::CommandValidator] before running | ||
# them. | ||
# * It will execute the commands in {protected mode}[rdoc-ref:Console1984::Shield#with_protected_mode] | ||
# if needed. | ||
# * It will log the command execution, and flag suspicious attempts and forbidden commands | ||
# appropriately. | ||
class Console1984::CommandExecutor | ||
include Console1984::Freezeable | ||
|
||
delegate :username_resolver, :session_logger, :shield, to: Console1984 | ||
|
||
# Logs and validates +commands+, and executes the passed block in a protected environment. | ||
# | ||
# Suspicious commands will be executed but flagged as suspicious. Forbidden commands will | ||
# be prevented and flagged too. | ||
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 | ||
flag_suspicious(commands) | ||
rescue Console1984::Errors::SuspiciousCommand | ||
flag_suspicious(commands) | ||
execute_in_protected_mode(&block) | ||
rescue FrozenError | ||
flag_suspicious(commands) | ||
ensure | ||
run_as_system { session_logger.after_executing commands } | ||
end | ||
|
||
# Executes the passed block in protected mode. | ||
# | ||
# See Console1984::Shield::Modes. | ||
def execute_in_protected_mode(&block) | ||
run_as_user do | ||
shield.with_protected_mode(&block) | ||
end | ||
end | ||
|
||
# Executes the passed block as a user. | ||
# | ||
# While the block is being executed, #executing_user_command? will return true. | ||
# This method helps implementing certain protection mechanisms that should only act with | ||
# user commands. | ||
def run_as_user(&block) | ||
run_command true, &block | ||
end | ||
|
||
# Executes the passed block as the system. | ||
# | ||
# While the block is being executed, #executing_user_command? will return false. | ||
def run_as_system(&block) | ||
run_command false, &block | ||
end | ||
|
||
# Returns whether the system is currently executing a user command. | ||
def executing_user_command? | ||
@executing_user_command | ||
end | ||
|
||
# Validates the command. | ||
# | ||
# See Console1984::CommandValidator. | ||
def validate_command(command) | ||
command_validator.validate(command) | ||
end | ||
|
||
private | ||
COMMAND_VALIDATOR_CONFIG_FILE_PATH = Console1984::Engine.root.join("config/command_protections.yml") | ||
|
||
def command_validator | ||
@command_validator ||= build_command_validator | ||
end | ||
|
||
def build_command_validator | ||
Console1984::CommandValidator.from_config(YAML.safe_load(File.read(COMMAND_VALIDATOR_CONFIG_FILE_PATH)).symbolize_keys) | ||
end | ||
|
||
def flag_suspicious(commands) | ||
puts "Forbidden command attempted: #{commands.join("\n")}" | ||
run_as_system { session_logger.suspicious_commands_attempted commands } | ||
nil | ||
end | ||
|
||
def run_command(run_by_user, &block) | ||
original_value = @executing_user_command | ||
@executing_user_command = run_by_user | ||
block.call | ||
ensure | ||
@executing_user_command = original_value | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
# Validates console commands. | ||
# | ||
# This performs an static analysis of console commands. The analysis is meant to happen | ||
# *before* commands are executed, so that they can prevent the execution if needed. | ||
# | ||
# 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). | ||
# | ||
# 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. | ||
# | ||
# There is a convenience method .from_config that lets you instantiate a validation setup from | ||
# a config hash (e.g to customize validations via YAML). | ||
# | ||
# See +config/command_protections.yml+ and the validations in +lib/console1984/command_validator+. | ||
class Console1984::CommandValidator | ||
include Console1984::Freezeable | ||
|
||
def initialize | ||
@validations_by_name = HashWithIndifferentAccess.new | ||
end | ||
|
||
class << self | ||
# Instantiates a command validator that will configure the validations based on the config passed. | ||
# | ||
# For each key in +config+, it will derive the class Console1984::CommandValidator::#{key.camelize}Validation | ||
# and will instantiate the validation passed the values as params. | ||
# | ||
# For example for this config: | ||
# | ||
# { forbidden_reopening: [ActiveRecord, Console1984] } | ||
# | ||
# It will instantiate Console1984::CommandValidator::ForbiddenReopeningValidation passing | ||
# +["ActiveRecord", "Console1984"]+ in the constructor. | ||
# | ||
# # See +config/command_protections.yml+ as an example. | ||
def from_config(config) | ||
Console1984::CommandValidator.new.tap do |validator| | ||
config.each do |validator_name, validator_config| | ||
validator_class = "Console1984::CommandValidator::#{validator_name.to_s.camelize}Validation".constantize | ||
validator_config.try(:symbolize_keys!) | ||
validator.add_validation validator_name, validator_class.new(validator_config) | ||
end | ||
end | ||
end | ||
end | ||
|
||
# Adds a +validation+ to the chain indexed by the provided +name+ | ||
# | ||
# Validations are executed in the order they are added. | ||
def add_validation(name, validation) | ||
validations_by_name[name] = validation | ||
end | ||
|
||
# Executes the chain of validations passing a {parsed command}[rdoc-ref:Console1984::CommandValidator::ParsedCommand] | ||
# created with the +command+ string passed by parameter. | ||
# | ||
# The validations are executed in the order they were added. If one validation raises an error, the error will | ||
# raise and the rest of validations won't get checked. | ||
def validate(command) | ||
parsed_command = ParsedCommand.new(command) | ||
|
||
validations_by_name.values.each do |validation| | ||
validation.validate(parsed_command) | ||
end | ||
end | ||
|
||
private | ||
attr_reader :validations_by_name | ||
end |
31 changes: 31 additions & 0 deletions
31
lib/console1984/command_validator/forbidden_constant_reference_validation.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Validates references to a configured set of constants. | ||
class Console1984::CommandValidator::ForbiddenConstantReferenceValidation | ||
include Console1984::Freezeable | ||
|
||
# +config+ will be a hash like: | ||
# | ||
# { always: [ Console1984 ], protected: [ PG, Mysql2 ] } | ||
def initialize(shield = Console1984.shield, config) | ||
# We make shield an injectable dependency for testing purposes. Everything is frozen | ||
# for security purposes, so stubbing won't work. | ||
@shield = shield | ||
|
||
@forbidden_constants_names = config[:always] || [] | ||
@constant_names_forbidden_in_protected_mode = config[:protected] || [] | ||
end | ||
|
||
# Raises a Console1984::Errors::ForbiddenCommand 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 | ||
end | ||
end | ||
|
||
private | ||
def contains_invalid_const_reference?(parsed_command, banned_constants) | ||
parsed_command.constants.find do |constant_name| | ||
banned_constants.find { |banned_constant| "#{constant_name}::".start_with?("#{banned_constant}::") } | ||
end | ||
end | ||
end |
29 changes: 29 additions & 0 deletions
29
lib/console1984/command_validator/forbidden_reopening_validation.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Validates attempts to reopen classes and modules based on a configured set. | ||
class Console1984::CommandValidator::ForbiddenReopeningValidation | ||
include Console1984::Freezeable | ||
|
||
attr_reader :banned_class_or_module_names | ||
|
||
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 | ||
# is detected. | ||
def validate(parsed_command) | ||
if contains_invalid_class_or_module_declaration?(parsed_command) | ||
raise Console1984::Errors::ForbiddenCommand | ||
end | ||
end | ||
|
||
private | ||
def contains_invalid_class_or_module_declaration?(parsed_command) | ||
parsed_command.declared_classes_or_modules.find { |class_or_module_name| banned?(class_or_module_name) } | ||
end | ||
|
||
def banned?(class_or_module_name) | ||
@banned_class_or_module_names.find do |banned_class_or_module_name| | ||
"#{class_or_module_name}::".start_with?("#{banned_class_or_module_name}::") | ||
end | ||
end | ||
end |
Oops, something went wrong.