Skip to content

Instantly share code, notes, and snippets.

@nanxstats
Created June 30, 2023 17:15
Show Gist options
  • Save nanxstats/cb743265f7d9102bcb060389e7512c6b to your computer and use it in GitHub Desktop.
Save nanxstats/cb743265f7d9102bcb060389e7512c6b to your computer and use it in GitHub Desktop.
Use `rlang::eval_tidy()` safely [DRAFT]
# Use `rlang::eval_tidy()` safely
```{r setup, include=FALSE}
knitr::opts_chunk$set(comment = "#>", collapse = TRUE, error = TRUE)
```
`rlang::eval_tidy()` is a powerful tool that allows you to evaluate R code
within a specific environment, often a data frame or a list.
However, if used carelessly with user input, it can introduce
security vulnerabilities.
The key to using `rlang::eval_tidy()` securely is to:
- Control the environment in which expressions are evaluated strictly.
- Validate any expressions before they are evaluated.
- Avoid exposing any internal variables or functions that could be misused.
Several usage patterns to mitigate these risks are summarized below.
## Avoid evaluating user input
It is generally not a good idea to execute arbitrary code provided by the user.
Bad usage:
```{r}
user_input <- "write.csv(mtcars, file = tempfile())"
user_expression <- rlang::parse_expr(user_input)
rlang::eval_tidy(user_expression)
```
The above code could execute any code entered by the user, including
potentially malicious code.
## Restrict the environment where code is evaluated
You can limit the scope of what can be done by supplying a strictly defined
environment to `rlang::eval_tidy()`.
```{r}
env <- rlang::env(x = 10, y = 20)
mask <- rlang::new_data_mask(env)
expr <- rlang::parse_expr("x + y")
rlang::eval_tidy(expr, mask)
```
Note that this can still access the variables in the parent environment.
## Validate expressions
If user-provided expressions need to be evaluated, validate them first to ensure they only contain permitted operations.
```{r}
user_input <- "write.csv(mtcars, file = tempfile())"
user_expression <- rlang::parse_expr(user_input)
# Check that the expression only contains operations you are expecting
allowed_names <- c("a", "b", "+", "-", "*", "/")
if (!all(all.names(user_expression) %in% allowed_names)) stop("Invalid expression", call. = FALSE)
rlang::eval_tidy(user_expression, mask)
```
## Use `rlang::call2()` cautiously
`rlang::call2()` can be used to create and evaluate a function call from an
expression and the function argument values. The function call is constructed
in a way that allows easy manipulation of arguments.
```{r}
# Define the function
fn <- function(a, b) a + b
# Create a call
expr <- rlang::call2("fn", a = 10, b = 20)
# Validate expression
allowed_names <- c("fn")
if (!all(all.names(expr) %in% allowed_names)) stop("Invalid expression", call. = FALSE)
# Evaluate the call
rlang::eval_tidy(expr, env = rlang::env(fn = fn))
```
When combined with `rlang::is_installed()` and `rlang::eval_tidy()`,
it can be used to create wrapper functions based on functions from
other namespaces without importing the functions explicitly, to make them
runtime dependencies.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment