Ruby\Rails sandboxing - ruby-on-rails

I have a case, when the text from the DB field should be "evaled" in the sandbox mode - with whitelist of methods and constants, allowed to invoke.
Gem https://github.com/tario/shikashi fits to this perfectly, but it seems to me, that it's abandoned.
I even can't use run Basic Example 2 (only Basic Example 1 works fine):
require "rubygems"
require "shikashi"
include Shikashi
def foo
# privileged code, can do any operation
print "foo\n"
end
s = Sandbox.new
priv = Privileges.new
# allow execution of foo in this object
priv.object(self).allow :foo
# allow execution of method :times on instances of Fixnum
priv.instances_of(Fixnum).allow :times
#inside the sandbox, only can use method foo on main and method times on instances of Fixnum
s.run(priv, "2.times do foo end")
Because it fails with an Error Cannot invoke method foo on object of class Object (SecurityError)
This gem uses another gem evalhook that looks for me complicated in order to fix the issue. There are another gems like this one but they are even more abandoned.
As far as I understood using $SAFE is not a good idea, because it has vulnerabilities.
Are there another approaches for such feature? Maybe manipulating with Binding object?

My problem was not so hard to solve without any gems. The idea is that you have whitelist of methods and constants WHITELIST and this class checks, if all methods and constants are in the whitelist
# gem install 'parser'
require 'parser/current'
class CodeValidator
attr_reader :errors, :source
WHITELIST = {:send => [:puts, :+, :new], :const => [:String]}
class Parser::AST::Node
def value
return children[1] if [:send, :const].include? type
fail NotImplementedError
end
end
def initialize(source)
#errors = []
#source = source
end
def valid?
!insecure_node?(root_node)
end
private
def exclude_node?(node)
blacklisted_node_types.include?(node.type) && !WHITELIST[node.type].include?(node.value)
end
def blacklisted_node_types
WHITELIST.keys
end
def insecure_node?(node)
return !!add_error_for_node(node) if exclude_node?(node)
node.children.each { |child_node| return true if child_node.class == Parser::AST::Node && insecure_node?(child_node) }
false
end
def root_node
#root_node ||= Parser::CurrentRuby.parse source
end
def add_error_for_node(node)
errors << "#{node.type} not allowed: #{node.value}"
end
end
c = CodeValidator.new("s = 'hello ' + String.new('world'); puts s.inspect")
p c.valid? # => false
p c.errors # => ["send not allowed: inspect"]

Related

how to type strong params in rails api / sorbet / tapioca

i'm trying to start a new project where i play with typing and i'm having a hard time getting the basic API stuff to work.
here is the code from my controller:
# typed: strict
module Api
module V1
class PlayersController < ApplicationController
extend T::Sig
sig { returns(String) }
def create
player = Player.create(player_params)
render json: player
end
private
sig { returns(ActionController::Parameters) }
def player_params
params.require(:player).permit(:name, :email)
end
end
end
end
i'm getting an error on the permit call.
Method `permit` does not exist on `String` component of
`T.any(String, Numeric, T::Array[T.untyped], ActionController::Parameters)`
i've tried passing in params into the player_params method and assigning it to a variable that has a type of ActionController::Parameters, but that didn't work either.
if there's a doc or suggestion, i'd love to learn where i'm going wrong!
for additional context i am using in my gemfile
gem 'sorbet', :group => :development
gem 'sorbet-runtime'
gem 'tapioca', require: false, :group => :development
ok, so i was close when i was trying to assign the params variable to a typed variable.
solution:
def player_params
player_params = T.cast(params.require(:player), ActionController::Parameters)
player_params.permit(:name, :email)
end
require can return any of those types T.any(String, Numeric,
T::Array[T.untyped], ActionController::Parameters). As Sorbet can't
know which one will be returned at runtime it try to see if the method
called on that return exists in all types, which clearly don't.
To fix you need to tell sorbet what the return of require is going to
be at runtime using T.cast
i ended up asking on the repo: https://github.com/Shopify/tapioca/issues/1122

Any way to run a block right after Chewy `udpate_index`?

In a Rails app Chewy gem is used to handle ElasticSearch indexing.
I have a block of code in an after_commit and I need it to be run once a new record of the RDB is indexed in our NRDB. it looks like:
class User < ApplicationRecord
update_index('USER') { self }
after_commit :run_this_block, on: :create
def run_this_block
index = UsersIndex.find id
'do something with index'
end
end
seems the after_commit is called before update_index!!!
Nothing is found in chewy gem,
Anyone with any idea?
That could be a possible solution, though it doesn’t work in my specific case:
There is leave method in atomic strategy of Chewy which let bypass the main atomic update_index action:
class Atomic < Base
def initialize
#stash = {}
end
def update(type, objects, _options = {})
#stash[type] ||= []
#stash[type] |= type.root.id ? Array.wrap(objects) : type.adapter.identify(objects)
end
def leave
#stash.all? { |type, ids| type.import!(ids) }
end
end
It is possible to prepend leave method in chewy.rb:
# /config/initializers/chewy.rb
Chewy::Strategy::Atomic.prepend(
Module.new do
def leave
#stash.all? do |type, ids|
if INTENDED_INDICES_NAMES.include?(type.name)
type.import!(sliced_ids)
<here goes the code where one have access to recently indexed records>
else
type.import!(ids)
end
true
end
end
end

Overriding initialize error test environment

I am having a hard time understanding an error with ruby's open classes. Overriding a method in test environment (rails) causes an invalid arguments error on initialize. For some reason, overriding the new method solved the problem. Why?
My class:
require 'adwords_api'
require 'adwords_config'
require 'scanf'
class AdwordsHelper
attr_accessor :adwords_id
attr_accessor :campaign_id
attr_accessor :adgroup_id
attr_accessor :invalid_ops
##RETRY_INTERVAL = 3
##RETRIES_COUNT = 500
##config = 'adwords_api.yml'
def initialize(args = {})
self.adwords_id = AdwordsConfig.config[:master_account]
return unless args.is_a?(Hash)
args.each do |k,v|
instance_variable_set("##{k}", v) unless v.nil?
end
end
def set_budget(budget)
#api = get_adwords_api
budget_id = get_budget_id
service = #api.service(:BudgetService)
operation = {
:operator => 'SET',
:operand => {
:budget_id => budget_id,
:amount => {
:micro_amount => budget
}
}
}
service.mutate([operation])
end
end
Now in the test, I want to override one of the methods, because I don't want there to be communication with the actual adwords servers.
require "test_helper"
# FIXME override this in another file eventually
class AdwordsHelper
def set_budget(budget)
true
end
end
Calling
AdwordsHelper.new(campaign_id: 1)
gives an invalid arguments 1 for 0 error.
Now, overriding the new function, solves the error.
class AdwordsHelper
def self.new(*args, &block)
obj = allocate
obj
end
def set_budget(budget)
true
end
end
Is overriding the method set_budget causing other quirks?
Try
AdwordHelper.class_eval do
def set_budget(budget)
true
end
end
class AdwordHelper will define the class when the original AdwordHelper is not loaded already, in which case the only method that exists in your class is set_budget. It wouldn't know about the initialize with one argument.
Whereas, if you use class_eval, it tries to load the file containing AdwordHelper class and then add the method set_budget to the class.

Rails - NoMethodError

I am receiving NoMethodErrors when my DeltaSynWorker runs. This happens in an application that was built as a revision of a currently working application. I am not the original programmer, and I am coming at it from a Java background (I mention this because it is possible I am missing something very obvious to others). I cannot figure out why NoMethodeError is being thrown when the code is very similar to code that is currently working fine in a different web application.
The Error:
NoMethodError: undefined method `client' for #<ApiRequestBuilder:0x0000000705a8f0>
delta_sync_worker.rb
class DeltaSyncWorker < SyncWorker
include Sidekiq::Worker
sidekiq_options queue: "delta_syncs"
def perform(subscriber_id, client_id)
sleep(10)
current_subscriber = ApiSubscriberDecorator.decorate(Subscriber.find(subscriber_id))
Time.zone = current_subscriber.time_zone
client = ApiClientDecorator.decorate(Client.find(client_id))
arb = ApiRequestBuilder.new(URI.parse(SR_HOST + '/servlet/sync/process'))
if arb.client(:subscriber => current_subscriber, :client => client)
arb.transmit
if arb.success?
current_subscriber.touch(:sync_updated_at)
decorated_client = ClientDecorator.decorate(client.model)
ConfirmationsSyncWorker.perform_in(1.hours, current_subscriber.id)
else
error_params = {:subscriber => current_subscriber.id, :response_body => arb.response.body, :request_body => arb.request.body, :sync_type => "DeltaSyncWorker"}
Airbrake.notify(:error_class => "sync_error", :error_message => "Sync Error: #{arb.response.message}", :params => error_params)
end
end
end
end
api_request_builder.rb
require 'nokogiri'
class ApiRequestBuilder < AbstractController::Base
include AbstractController::Rendering
include AbstractController::Layouts
include AbstractController::Helpers
include AbstractController::Translation
include AbstractController::AssetPaths
self.view_paths = "app/api"
attr_accessor :request_body, :request, :response, :append_request_headers, :request_method, :url
def initialize(url, *args)
#url = url
if args
args.each do |arg|
arg.each_pair{ |k,v| instance_variable_set("##{k.to_s}", v) }
end
end
end
# this will search for an api request template in api/requests, render that template and set any instance variables
def method_missing(meth, *args, &block)
if lookup_context.template_exists?("requests/#{meth.to_s}")
if args
args.each do |arg|
arg.each_pair{|k,v| instance_variable_set("##{k.to_s}", v) }
end
end
#request_body = (render :template => "requests/#{meth.to_s}")
else
super
end
end
def transmit
#request_method ||= "Post"
#request = "Net::HTTP::#{#request_method}".constantize.new(#url.path)
#request['x-ss-user'] = #subscriber.sr_user if #subscriber && #subscriber.sr_user.present?
#request['x-ss-pwd'] = #subscriber.sr_password if #subscriber && #subscriber.sr_password.present?
unless #append_request_headers.nil?
#append_request_headers.each_pair{ |k,v| #request[k] = v }
end
#request.body = #request_body if request_body? && #request.request_body_permitted?
#http = Net::HTTP.new(#url.host, #url.port)
#http.use_ssl = true
#http.verify_mode = OpenSSL::SSL::VERIFY_NONE
#response = #http.request(#request)
end
def success?
if #response.code == 200.to_s
return true
else
return false
end
end
def request_body?
unless #request_body.nil?
return true
else
return false
end
end
end
I have been looking at other NoMethodError questions here, but I cannot find an answer I feel applies to my situation. Any help is greatly appreciated. Thanks!
method_missing will catch sent messages for which there is no method defined, and the call to super at the end will pass it up to Ruby's standard method_missing behavior, which is what you are seeing (NoMethodError). However, this only happens if the if condition is not met, which is what I suspect is happening here, and would explain why it works in some situations but not in others. The call to :client, having found no matching methods along the inheritance chain, will look for a template called "requests/client" - try adding this template and see if that fixes the issue.
I know Ive seen this before and I feel like it wasn't what it appeared to be, but ya basically method missing is just intercepting the method call and when you call arb.client, it is caught by method missing and therefore tries to render api/client.xml.arb or api whatever the file type is. -- Note that there should be a file in the initializers directory named somethig like api_template_handler.rb or arb_temmplate_handler.rb, which is what allows rails to see that template type in the view directory -- make sure that is there first. Also sidenote, _client.xml.api is a partial used by the other api request in that directory (full sync),
To debug Id start by, above the following line
if lookup_context.template_exists?("requests/#{meth.to_s}")
Add a log statement
Rails.logger.debug "Can I see View?" + lookup_context.template_exists?("requests/#{meth.to_s}")
If true, then the problem is the error isnt getting caught properly because of method missing. If false, then the sidekiq worker isnt loading rails properly, or the view path isn't being added onto the rails view paths.
If true, Im guessing it might have something to do with the client model not being loaded, or an attribute on the client model not existing, that the builder is trying to call, and the error is somehow bubbling up to the api request builder class.
Oh also, just general stuff, but make sure redis and sidekiq are running, and restart passenger if its non local environment.
Let me know how it goes.

Rails Metaprogramming: How to add instance methods at runtime?

I'm defining my own AR class in Rails that will include dynamically created instance methods for user fields 0-9. The user fields are not stored in the db directly, they'll be serialized together since they'll be used infrequently. Is the following the best way to do this? Alternatives?
Where should the start up code for adding the methods be called from?
class Info < ActiveRecord::Base
end
# called from an init file to add the instance methods
parts = []
(0..9).each do |i|
parts.push "def user_field_#{i}" # def user_field_0
parts.push "get_user_fields && #user_fields[#{i}]"
parts.push "end"
end
Info.class_eval parts.join
One nice way, especially if you might have more than 0..9 user fields, would be to use method_missing:
class Info
USER_FIELD_METHOD = /^user_field_(\n+)$/
def method_missing(method, *arg)
return super unless method =~ USER_FIELD_METHOD
i = Regexp.last_match[1].to_i
get_user_fields && #user_fields[i]
end
# Useful in 1.9.2, or with backports gem:
def respond_to_missing?(method, private)
super || method =~ USER_FIELD_METHOD
end
end
If you prefer to define methods:
10.times do |i|
Info.class_eval do
define_method :"user_field_#{i}" do
get_user_fields && #user_fields[i]
end
end
end
Using method_missing is very difficult to maintain and unnecessary. The other alternative using define_method is better but leads to poorly performing code. The following 1 liner is all you need:
class Info
end
Info.class_eval 10.times.inject("") {|s,i| s += <<END}
def user_field_#{i}
puts "in user_field_#{i}"
end
END
puts Info.new.user_field_4

Resources