Secure way to accept user input for shiny apps

highlight

#1

Hello ...
I am using R shiny to develop mathematics APPS where I use the function eval() to convert string numbers from user to numeric inputs in R. For example, to accept the vector: v = (3, 4/11, pi/2, sqrt(13)), I use the textInput function then convert it to numeric values using .the following code.

_v = as.character(input$v)_
_v = strsplit(v,",")_
_v = v[[1]]_
_vv = v_
_vi = rep(0,times = length(v))_
_for (i in 1:length(vv)){_
_  vi[i] = eval(parse(text = vv[i])) # convert to numeric ***_
_}_
_v = vi_

My problem is that I read on Stackflow that eval() function should never be used if an R shiny app is to be hosted online. Is there a secure way to capture this input.

I also use the same to accept mathematical functions. For example, a user enters a function like exp(x) * sin(x) in the input field, then I convert it to an R function using the line below.

_f = function(x) {eval(parse(text = input$func))}_

Thank you for any help or suggestion.


#2

One idea that comes to mind is to interpret the R expression manually and impose some of your own restrictions. Here's an example:

library(rlang)

interpret <- function(expr_str, 
                      max_length = 32, 
                      whitelist = c("/", "*", "sqrt", "c")) {
  safer_eval <- function(expr) {
    if (rlang::is_call(expr)) {
      fn_name <- rlang::call_name(expr)
      if (!fn_name %in% whitelist) stop("Disallowed function: ", fn_name)
      do.call(get(fn_name, baseenv()), Map(safer_eval, rlang::call_args(expr)))
    } else if (rlang::is_syntactic_literal(expr)) {
      expr
    } else {
      stop("Unknown expression: ", expr)
    }
  }
  stopifnot(length(expr_str) < max_length)
  safer_eval(rlang::parse_expr(expr_str))
}

# > interpret("c(2*3, 12)")
# [1]  6 12

# > interpret("c(2*3, 12, system('rm /tmp/foo'))")
#  Error in (function (expr, env)  : Disallowed function: system 
  • If the input string is longer than max_length, it throws an exception. This prevents people from submitting very large strings that could tie up your R process with parsing.
  • Recognize only a subset of R functions. In particular, those named by whitelist that exist in base. This prevents people from calling functions like base::system. It's important that these functions don't accept quoted arguments that are evaluated separately, since that could defeat the whitelist approach.
  • Anything other than a function call or a syntactic literal throws the Unknown expression exception, so variables are not resolved and can't be created.

Since functions can't be defined and loops can't be created, I don't think it would be possible to tie up R with a busy loop or infinite recursion, and so we don't need to impose a limit on running time. We wouldn't be able to do that anyway without forking a new R process, or without writing a more involved interpreter or VM that maintained its own code vector and argument stack.

One extension point you might consider is, instead of throwing Unknown expression, interpret could accept the input object as a parameter, and you could resolve symbols to members of the input object. This way, your interpreted programs could participate in reactivity.

In the course of researching this I was directed to https://github.com/jeroen/RAppArmor by a co-worker, but it looks much more involved to set up, but is probably the "complete" answer, particularly if you have control over your hosting environment and are on Linux.


#3

Thanks very much alandipert for the detailed and informative response. That solves my concern.