2

AKA How do I find an unescaped character sequence with regex?

Given an environment set up with:

@secret = "OH NO!"
$secret = "OH NO!"
@@secret = "OH NO!"

and given string read in from a file that looks like this:

some_str = '"\"#{:NOT&&:very}\" bad. \u262E\n#@secret \\#$secret \\\\#@@secret"'

I want to evaluate this as a Ruby string, but without interpolation. Thus, the result should be:

puts safe_eval(some_str)
#=> "#{:NOT&&:very}" bad. ☮
#=> #@secret #$secret \#@@secret

By contrast, the eval-only solution produces

puts eval(some_str)
#=> "very" bad. ☮
#=> OH NO! #$secret \OH NO!

At first I tried:

def safe_eval(str)
  eval str.gsub(/#(?=[{@$])/,'\\#')
end

but this fails in the malicious middle case above, producing:

#=> "#{:NOT&&:very}" bad. ☮
#=> #@secret \OH NO! \#@@secret
1
  • The motivation for this question is a simple way to safely implement the answer to this question. Commented May 22, 2013 at 15:51

2 Answers 2

1

You can do this via regex by ensuring that there are an even number of backslashes before the character you want to escape:

def safe_eval(str)
  eval str.gsub( /([^\\](?:\\\\)*)#(?=[{@$])/, '\1\#' )
end

…which says:

  • Find a character that is not a backslash [^\\]
  • followed by two backslashes (?:\\\\)
    • repeated zero or more times *
  • followed by a literal # character
  • and ensure that after that you can see either a {, @, or $ character.
  • and replace that with
    • the non-backslash-maybe-followed-by-even-number-of-backslashes
    • and then a backslash and then a #
Sign up to request clarification or add additional context in comments.

9 Comments

But am I missing an attack vector for running arbitrary Ruby code?
@Phogrez: If you're running this on a server anywhere, say goodbye to your data. If this is just a local app, this is good enough.
@Linuxios "Maybe before we rush to adopt eval we should stop to consider the consequences of blithely giving this technology such a central position in our server"? Which is to say, "It sounds like you're giving good advice, but all you seem to actually be doing is fearmongering based on the presence of eval. Could you please provide a concrete attack vector that this does not guard against?"
@Phogrez: Agree with the XKCD, Let me think on this for a moment.
What's preventing someone from just puttong some Ruby code instead of a string? E.g.: some_str = 'system('rm -rf /')?
|
1

How about not using eval at all? As per this comment in chat, all that's necessary are escaping quotes, newlines, and unicode characters. Here's my solution:

ESCAPE_TABLE = {
  /\\n/ => "\n",
  /\\"/ => "\"",
}
def expand_escapes(str)
  str = str.dup
  ESCAPE_TABLE.each {|k, v| str.gsub!(k, v)}
  #Deal with Unicode
  str.gsub!(/\\u([0-9A-Z]{4})/) {|m| [m[2..5].hex].pack("U") }
  str
end

When called on your string the result is (in your variable environment):

"\"\"\#{:NOT&&:very}\" bad. ☮\n\#@secret \\\#$secret \\\\\#@@secret\""

Although I would have preferred not to have to treat unicode specially, it is the only way to do it without eval.

3 Comments

FWIW, the chat comment was meant as an example, not a full list. All escape sequences need to be supported, e.g. \t and \r and \s and such. But that's obviously extensible in your solution.
Instead of n gsubs, one per replacement, though, I would suggest a single gsub with the block form that uses the lookup table to determine the replacement string.
@Phrogz: That's a good idea. I'll do it later, I'm a little busy right now.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.