
Security News
Follow-up and Clarification on Recent Malicious Ruby Gems Campaign
A clarification on our recent research investigating 60 malicious Ruby gems.
Myrrha provides the coercion framework which is missing to Ruby, IMHO. Coercions are simply defined as a set of rules for converting values from source to target domains (in an abstract sense). As a typical and useful example, it comes bundled with a coerce() method providing a unique entry point for converting a string to a numeric, a boolean, a date, a time, an URI, and so on.
% [sudo] gem install myrrha
# Bug fixes (tiny) do not even add new default rules to coerce and
# to\_ruby\_literal. Minor version can, which could break your code.
# Therefore, please always use:
gem "myrrha", "~> 1.2.2"
coerce()
featureMyrrha.coerce(:anything, Domain)
coerce(:anything, Domain) # with core extensions
Having a single entry point for coercing values from one data-type (typically a String) to another one is very useful. Unfortunately, Ruby does not provide such a unique entry point... Thanks to Myrrah, the following scenario is possible and even straightforward:
require 'myrrha/with_core_ext'
require 'myrrha/coerce'
require 'date'
values = ["12", "true", "2011-07-20"]
types = [Integer, Boolean, Date]
values.zip(types).collect do |value,domain|
coerce(value, domain)
end
# => [12, true, #<Date: 2011-07-20 (...)>]
Implemented coercions are somewhat conservative, and only use a subset of what ruby provides here and there. This is to avoid strangeness ala PHP... The general philosophy is to provide the natural coercions we apply everyday.
The master rules are
coerce(value, Domain)
returns value
if
belongs_to?(value, Domain)
is true (see last section below)coerce(value, Domain)
returns Domain.coerce(value)
if the latter method exists.coerce("any string", Domain)
returns Domain.parse(value)
if the latter method exists.The specific implemented rules are
require 'myrrha/with_core_ext'
require 'myrrha/coerce'
# NilClass -> _Anything_ returns nil, always
coerce(nil, Integer) # => nil
# Object -> String, via ruby's String()
coerce("hello", String) # => "hello"
coerce(:hello, String) # => "hello"
# String -> Numeric, through ruby's Integer() and Float()
coerce("12", Integer) # => 12
coerce("12.0", Float) # => 12.0
# String -> Numeric is smart enough:
coerce("12", Numeric) # => 12 (Integer)
coerce("12.0", Numeric) # => 12.0 (Float)
# String -> Regexp, through Regexp.compile
coerce("[a-z]+", Regexp) # => /[a-z]+/
# String -> Symbol, through to_sym
coerce("hello", Symbol) # => :hello
# String -> Boolean (hum, sorry Matz!)
coerce("true", Boolean) # => true
coerce("false", Boolean) # => false
coerce("true", TrueClass) # => true
coerce("false", FalseClass) # => false
# String -> Date, through Date.parse
require 'date'
coerce("2011-07-20", Date) # => #<Date: 2011-07-20 (4911525/2,0,2299161)>
# String -> Time, through Time.parse (just in time issuing of require('time'))
coerce("2011-07-20 10:57", Time) # => 2011-07-20 10:57:00 +0200
# String -> URI, through URI.parse
require 'uri'
coerce('http://google.com', URI) # => #<URI::HTTP:0x8281ce0 URL:http://google.com>
# String -> Class and Module through constant lookup
coerce("Integer", Class) # => Integer
coerce("Myrrha::Version", Module) # => Myrrha::Version
# Symbol -> Class and Module through constant lookup
coerce(:Integer, Class) # => Integer
coerce(:Enumerable, Module) # => Enumerable
require 'myrrha/coerce'
Myrrha.coerce("12", Integer) # => 12
Myrrha.coerce("12.0", Float) # => 12.0
Myrrha.coerce("true", Myrrha::Boolean) # => true
# [... and so on ...]
The easiest way to add additional coercions is to implement a coerce
method on you class; it will be used in priority.
class Foo
def initialize(arg)
@arg = arg
end
def self.coerce(arg)
Foo.new(arg)
end
end
Myrrha.coerce(:hello, Foo)
# => #<Foo:0x869eee0 @arg=:hello>
If Foo
is not your code and you don't want to monkey patch the class
by adding a coerce
class method, you can simply add new rules to
Myrrha itself:
Myrrha::Coerce.append do |r|
r.coercion(Symbol, Foo) do |value, _|
Foo.new(value)
end
end
Myrrha.coerce(:hello, Foo)
# => #<Foo:0x8866f84 @arg=:hello>
Now, doing so, the new coercion rule will be shared with all Myrrha users, which might be intrusive. Why not using your own set of coercion rules?
MyRules = Myrrha::Coerce.dup.append do |r|
r.coercion(Symbol, Foo) do |value, _|
Foo.new(value)
end
end
# Myrrha.coerce is actually a shortcut for:
Myrrha::Coerce.apply(:hello, Foo)
# => Myrrha::Error: Unable to coerce `hello` to Foo
MyRules.apply(:hello, Foo)
# => #<Foo:0x8b7d254 @arg=:hello>
to_ruby_literal()
featureMyrrha.to_ruby_literal([:anything])
[:anything].to_ruby_literal # with core extensions
Object#to_ruby_literal
has a very simple specification. Given an
object o that can be considered as a true value, the result of
o.to_ruby_literal
must be such that the following invariant
holds:
Kernel.eval(o.to_ruby_literal) == o
That is, parsing & evaluating the literal yields the same value. When generating (human-readable) ruby code, having a unique entry point that respects the specification is very useful.
For almost all ruby classes, but not all, using o.inspect respects the invariant. For example, the following is true:
Kernel.eval("hello".inspect) == "hello" # => true
Kernel.eval([1, 2, 3].inspect) == [1, 2, 3] # => true
Kernel.eval({:key => :value}.inspect) == {:key => :value} # => true
# => true
Unfortunately, this is not always the case:
Kernel.eval(Date.today.inspect) == Date.today
# => false
# => because Date.today.inspect yields "#<Date: 2011-07-20 ...", which is a comment
Myrrha implements a very simple set of rules for implementing
Object#to_ruby_literal
that works:
require 'date'
require 'myrrha/with_core_ext'
require 'myrrha/to_ruby_literal'
1.to_ruby_literal # => "1"
Date.today.to_ruby_literal # => "Marshal.load('...')"
["hello", Date.today].to_ruby_literal # => "['hello', Marshal.load('...')]"
Myrrha implements a best-effort strategy to return a human readable string. It
simply fallbacks to Marshal.load(...)
when the strategy fails:
(1..10).to_ruby_literal # => "1..10"
today = Date.today
(today..today+1).to_ruby_literal # => "Marshal.load('...')"
require 'date'
require 'myrrha/to_ruby_literal'
Myrrha.to_ruby_literal(1) # => 1
Myrrha.to_ruby_literal(Date.today) # => Marshal.load("...")
# [... and so on ...]
The easiest way is simply to override to_ruby_literal
in your
class
class Foo
attr_reader :arg
def initialize(arg)
@arg = arg
end
def to_ruby_literal
"Foo.new(#{arg.inspect})"
end
end
Myrrha.to_ruby_literal(Foo.new(:hello))
# => "Foo.new(:hello)"
As with coerce, contributing your own rule to Myrrha is possible:
Myrrha::ToRubyLiteral.append do |r|
r.coercion(Foo) do |foo, _|
"Foo.new(#{foo.arg.inspect})"
end
end
Myrrha.to_ruby_literal(Foo.new(:hello))
# => "Foo.new(:hello)"
And building your own set of rules is possible as well:
MyRules = Myrrha::ToRubyLiteral.dup.append do |r|
r.coercion(Foo) do |foo, _|
"Foo.new(#{foo.arg.inspect})"
end
end
# Myrrha.to_ruby_literal is actually a shortcut for:
Myrrha::ToRubyLiteral.apply(Foo.new(:hello))
# => "Marshal.load('...')"
MyRules.apply(Foo.new(:hello))
# => "Foo.new(:hello)"
As the feature fallbacks to marshaling, everything which is marshalable will
work. As usual, to_ruby_literal(Proc)
won't work.
A set of coercion rules can simply be created from scratch as follows:
Rules = Myrrha.coercions do |r|
# `upon` rules are tried in priority if PRE holds
r.upon(SourceDomain) do |value, requested_domain|
# PRE: - user wants to coerce `value` to a requested_domain
# - belongs_to?(value, SourceDomain)
# implement the coercion or throw(:newrule)
returned_value = something(value)
# POST: belongs_to?(returned_value, requested_domain)
end
# `coercion` rules are then tried in order if PRE holds
r.coercion(SourceDomain, TargetDomain) do |value, requested_domain|
# PRE: - user wants to coerce `value` to a requested_domain
# - belongs_to?(value, SourceDomain)
# - TargetDomain <=> requested_domain
# implement the coercion or throw(:newrule)
returned_value = something(value)
# POST: returned_value belongs either to TorgetDomain or to
# requested_domain (very smart converter)
end
# fallback rules are tried if everything else has failed
r.fallback(SourceDomain) do |value, requested_domain|
# exactly the same as upon rules
end
end
When the user invokes Rules.apply(value, domain)
all rules for
which PRE holds are executed in order, until one succeed (chain of
responsibility design pattern). This means that coercions always execute in
O(number of rules)
.
A converter is the third (resp. second) element specified in a coercion rules (resp. an upon or fallback rule). A converter is generally a Proc of arity 2, which is passed the source value and requested target domain.
Myrrha.coercions do |r|
r.coercion String, Numeric, lambda{|value,requested_domain|
# this is converter code
}
end
convert("12", Integer)
A converter may also be specified as an array of domains. In this case, it is assumed that they for a path inside the convertion graph. Consider for example the following coercion rules (contrived example)
rules = Myrrha.coercions do |r|
r.coercion String, Symbol, lambda{|s,t| s.to_sym } # 1
r.coercion Float, String, lambda{|s,t| s.to_s } # 2
r.coercion Integer, Float, lambda{|s,t| Float(s) } # 3
r.coercion Integer, Symbol, [Float, String] # 4
end
The last rule specifies a convertion path, through intermediate domains. The complete rule specifies that applying the following path will work
Integer -> Float -> String -> Symbol
#3 #2 #1
Indeed,
rules.coerce(12, Symbol) # => :"12.0"
belongs_to?
and subdomain?
The pseudo-code given above relies on two main abstractions. Suppose the user
makes a call to coerce(value, requested_domain)
:
belongs_to?(value, SourceDomain)
is true iif
SourceDomain
is a Proc
of arity 2, and
SourceDomain.call(value, requested_domain)
yields trueSourceDomain
is a Proc
of arity 1, and
SourceDomain.call(value)
yields trueSourceDomain === value
yields truesubdomain?(SourceDomain,TargetDomain)
is true iif
SourceDomain == TargetDomain
yields true:superdomain_of?
and answers true on
SourceDomainRules = Myrrha.coercions do |r|
# A 'catch-all' upon rule, always fired
catch_all = lambda{|v,rd| true}
r.upon(catch_all) do |value, requested_domain|
if you_can_coerce?(value)
# then do it!
else
throw(:next_rule)
end
end
# Delegate every call to the requested domain if it responds to compile
compilable = lambda{|v,rd| rd.respond_to?(:compile)}
r.upon(compilable) do |value, requested_domain|
requested_domain.compile(value)
end
# A fallback strategy if everything else fails
r.fallback(Object) do |value, requested_domain|
# always fired after everything else
# this is your last change, an Myrrha::Error will be raised if you fail
end
end
Specialization by constraint (SByC) is a theory of types for which the following rules hold:
For example, "positive integers" is a sub type of "integers" where the predicate is "value > 0".
Myrrha comes with a small feature allowing you to create types 'ala' SByC:
PosInt = Myrrha.domain(Integer){|i| i > 0}
PosInt.name # => "PosInt"
PosInt.class # => Class
PosInt.superclass # => Integer
PosInt.ancestors # => [PosInt, Integer, Numeric, Comparable, Object, Kernel, BasicObject]
PosInt === 10 # => true
PosInt === -1 # => false
PosInt.new(10) # => 10
PosInt.new(-10) # => ArgumentError, "Invalid value -10 for PosInt"
Note that the feature is very limited, and is not intended to provide a truly coherent typing framework. For example:
10.is_a?(PosInt) # => false
10.kind_of?(PosInt) # => false
Instead, Myrrha domains are only provided as an helper to build sound coercions rules easily while 1) keeping a Class-based approach to source and target domains and 2) having friendly error messages 3) really supporting true reasoning on types and value:
# Only a rule that converts String to Integer
rules = Myrrha.coercions do |r|
r.coercion String, Integer, lambda{|s,t| Integer(s)}
end
# it succeeds on both integers and positive integers
rules.coerce("12", Integer) # => 12
rules.coerce("12", PosInt) # => 12
# and correctly fails in each case!
rules.coerce("-12", Integer) # => -12
rules.coerce("-12", PosInt) # => ArgumentError, "Invalid value -12 for PosInt"
Note that if you want to provide additional tooling to your factored domain, the following way of creating them also works:
class PosInt < Integer
extend Myrrha::Domain.new(Integer, [], lambda{|i| i > 0})
end
FAQs
Unknown package
We found that myrrha demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
A clarification on our recent research investigating 60 malicious Ruby gems.
Security News
ESLint now supports parallel linting with a new --concurrency flag, delivering major speed gains and closing a 10-year-old feature request.
Research
/Security News
A malicious Go module posing as an SSH brute forcer exfiltrates stolen credentials to a Telegram bot controlled by a Russian-speaking threat actor.