Evaluate escaped string

Hello!

I have a form with a textarea field in which I would like to be able to pass interpolated strings.
The problem is that the string gets escaped and then it cannot be evaluated.

For example, the input Today’s date is #{Date.utc_today} gives “Today’s date is \ #{Date.utc_today}”,
(without the whitespace between \ and #)

Is there a way to “unescape” the string?

Thanks

Welcome to the forum!

When you say “unescape” do you mean interpolate the string and evaluate the code in the #{....} construct?

Not too difficult to do, but would be of quite some concern from a security point of view because now you have an opportunity for a user to execute arbitrary code on your server.

Or perhaps I have misunderstood?

2 Likes

What would happen if someone would write something like Real Slim Shady, please stand up! #{File.rm_rf("/")}?

3 Likes

Thanks!

Yes this is what I mean.

Let me give one more example to make it more clear.

Let’s say I have a directory with some files saved there, and I want to retrieve all the csv files having today’s date in their name. Then my input to the form field would be /path/to/dir/* #{Date.utc_today} * .csv,
which it would be escaped to /path/to/dir/*\ #{Date.utc_today} * .csv
So when I then call Path.wildcard(input), this would match no files since the # character is escaped and no interpolation will take place.

I know it bears security concerns, as any raw input, but let’s take for granted that the input comes only from trusted users

I believe the steps you would need to follow are:

  1. Unescape the string. Could be a simple as `String.replace(string, “\#{”, “#{”)
  2. Parse the string to extract the expressions. Interpolation happens only for string “constants”. Not input data. Could be as simple as String.split path, ~r/\#\{(.*)\}/, include_captures: true
  3. Evaluate the code. See Code.eval_string
  4. Recombine the list elements into a string

If input is from untrusted users, then follow this guide.

Use Code.string_to_quoted/2 to parse code. After that, you’d have to traverse an AST and ensure there are only allowed expressions. If everything is ok, then you can evaluate this AST using Code.eval_quoted/1.
Here is an example:

> ast_from_input = fn input -> ~s|"| <> String.replace(input, ~s|"|, ~s|\\"|) <> ~s|"| |> Code.string_to_quoted() end
#Function<7.126501267/1 in :erl_eval.expr/5>
> ~S|Today’s date is "#{Date.utc_today}"| |> ast_from_input.()
{:ok,
 {:<<>>, [line: 1],
  [
    "Today’s date is \"",
    {:"::", [line: 1],
     [
       {{:., [line: 1], [Kernel, :to_string]}, [line: 1],
        [
          {{:., [line: 1], [{:__aliases__, [line: 1], [:Date]}, :utc_today]},
           [no_parens: true, line: 1], []}
        ]},
       {:binary, [line: 1], nil}
     ]},
    "\""
  ]}}

in case of syntax error, a nice error is returned:

> ~S|Today’s date is "#{Date.utc_today/}"| |> ast_from_input.() 
{:error, {[line: 1, column: 36], "syntax error before: ", ""}}

and finally, evaluated string:

> ~S|Today’s date is "#{Date.utc_today}"| |> ast_from_input.() |> elem(1) |> Code.eval_quoted()
{"Today’s date is \"2021-06-09\"", []}

The only thing that is left here is to implement AST validator. Macro.traverse/4 can help with this task.

3 Likes

thanks my issue has been fixed.

How? Can you explain for the future readers? Or mark @fuelen’s answer as the solution?

"Today’s date is \ #{Date.utc_today}"  |>  :erlang.term_to_binary() |> :erlang.binary_to_term

result

"Today’s date is  2021-06-10"

TIL; ! Thanks for that very interesting snippet!

Exactly what I wanted.

Thank you very much!