Created
June 30, 2023 17:15
-
-
Save nanxstats/cb743265f7d9102bcb060389e7512c6b to your computer and use it in GitHub Desktop.
Use `rlang::eval_tidy()` safely [DRAFT]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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