7

How to convert string to ActiveSupport::Duration?

in rails console this code works

Date.today + 1.month (or 22.days)

but this not work

Date.today + '1.month' 

it says TypeError: expected numeric

'1.month' comes from db record.

Guru
  • 1,303
  • 18
  • 32

7 Answers7

4

As others have pointed out, using eval on your string creates a security vulnerability.

Instead, you can convert your string to an ActiveSupport::Duration using .to_i on the first part of your string to convert it to an Integer, and then .send the second part of your string to the integer to convert it to a Duration. Like this:

parts = '1.month'.split('.')
parts.first.to_i.send(parts.last)

For convenience, you could extend the String class by adding this file config/initializers/string.rb:

# Helper extension to String class
class String
  # Convert strings like "3.days" into Duration objects (via Integer)
  def to_duration
    super unless self =~ /\A\d+\.\w+\z/
    parts = split('.')

    allowed = %w(minutes hours days months years) # whitelist for .send
    super unless allowed.include? parts.last

    parts.first.to_i.send(parts.last)
  end
end

Then in your app, you can call .to_duration on strings.

Brent
  • 99
  • 7
  • Passing user input to `send` creates a security vulnerability as well: https://docs.ruby-lang.org/en/2.0.0/security_rdoc.html#label-send – Dan Bechard Oct 15 '19 at 19:47
  • Thank you. In this case, the input is being "sent" to the Integer class, so the risk is limited to its methods. I suppose the best way to clean this up would be to specify a whitelist of acceptable arguments: ```allowed = %w(minutes hours days months years)``` and then proceed only if `parts.last` were in that list. – Brent Oct 17 '19 at 20:23
  • On safety - the allowed list is still unsafe in that it allows for strings that have those values in them, like "weekdays". A safer solution would be to use a REGEX like `allowed = /\A(minute|hour|day|week|month|year)s?\z/`. Note, this also allows for plural values. – Troy Feb 27 '22 at 20:00
4

If you can choose the string format, then you should use ActiveSupport::Duration.parse, using the ISO8601 duration format: https://api.rubyonrails.org/classes/ActiveSupport/Duration.html#method-c-parse

Example:

ActiveSupport::Duration.parse('P1M')
=> 1 month
3

Yes, eval does the trick. But you should be sure the code in the database always is safe. If '22.days', '1.month' and so on come from user input, using eval is a huge security hole.

In this case try to use some kind of natural language parsers, like https://github.com/hpoydar/chronic_duration

dimuch
  • 12,728
  • 2
  • 39
  • 23
1

In Rails month or 'months' are Integer methods.

So when you use:

1.month

You are applying Integer#month to 1 (which is an integer).

However '1.month' is just a string. You can write anything between quotes and they are treated a String and not evaluated in any way unless you specifically ask for it.

eval('1.month') is one such instance where you specifically ask for your String to get evaluated and hence you get the desired result. However this can be dangerous as you do not have any check over your input String.

You can find a lot of references as to why eval is nasty.

In case you are doing this because your input is a String instead of Integer, you can always convert it to Integer using to_i. Example:

time = "1"
time.to_i.month 
#=> 2592000

This will work even for Integer values:

time = 1
time.to_i.month 
=> 2592000
Community
  • 1
  • 1
shivam
  • 16,048
  • 3
  • 56
  • 71
  • I need to convert int to months again? In databases I stored 1.minute, 22.days or even 10.months – Guru Dec 29 '15 at 08:26
  • Sorry I didn't see that. One thing you can do is write a rake task to replace all database string entries with respective integers. This will help you in long term. – shivam Dec 29 '15 at 08:31
0

If you have code like this Date.today + eval('1.month') (could lead to anybody executing any code they want on your server) what does it do? In the end its roughly the same as Date.today+30 (that is add 1.month to today's date.)

So you can get rid of the eval by instead storing perhaps just the days.

But it depends on your application, a date object plus 1.month actually moves the date out exactly 1 month (so if you start with Jan 29 2016 and add 3.months you get March 29, 2016 which is NOT 90 days away

So lets say your strings in that db are always 1.month or 2.months or 7.months (i.e. just some number of months) well in that case all you need to store in the DB is an integer value (or store it as a string like "7", then do "7".to_i)

But perhaps you want to allow months, days, weeks, and years... Well you could do this: {months: 7, days: 3, weeks: 1, years: 2}.to_json and store that in your db, then to use it you would say:

JSON.parse(some_string_from_db).inject(Date.today) do |date, inc|
  date+inc.last.send(inc.first.to_s) 
end

This is only marginally better than the eval since you are allowing arbitrary object and methods to be evaluated, but you could just add some protection like this:

JSON.parse(some_string_from_db).inject(Date.today) do |date, inc|
  if [:years, :months, :weeks, :days].include? inc.first.to_s
    date+inc.last.send(inc.first.to_s) 
  else
    Raise "don't try it buddy"
  end
end

Now you are back controlling what you are executing!

Of course it all depends on your application, but hopefully this answers your question.

Dorian
  • 22,759
  • 8
  • 120
  • 116
Mitch VanDuyn
  • 2,838
  • 1
  • 22
  • 29
0

You can store the human readable form in the DB, and then use the Fugit gem to parse the string into a duration - ActiveSupport::Duration does not seem to have as extensive parsing facilities. The Duration can be converted to seconds and then applied to normal Time/Date.

 deletion_deadline = "30 days"
 Time.zone.now + Fugit.parse_duration(deletion_deadline).to_sec
aqwan
  • 470
  • 4
  • 6
-3

Ohhooo I got answer on my own Need to add eval like this

Date.today + eval('1.month')

works perfectly fine

Is there any another way? what are pros and cons to use this way?

This could lead to a remote code execution exploit, e.g. random people doing whatever they want on your servers

Dorian
  • 22,759
  • 8
  • 120
  • 116
Guru
  • 1,303
  • 18
  • 32
  • `Date.today + 1.month` works just fine, this could easily lead to remote exploits (e.g. anybody executing random code on your server) – Dorian Jun 02 '17 at 18:52