Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@
behaviour. debian/ubuntu only; for alpine-based images you must
pass `user = NULL` and create the user yourself. Closes #100.

## Security

- `dock_from_desc()` and `dock_from_renv()` now validate every
user-supplied parameter that flows into a Dockerfile shell context
(`FROM`, `AS`, `repos` values and names, `extra_sysreqs`,
`renv_version`, `renv_paths_cache`, `lockfile` basename, `use_pak`,
`strict_install`, plus `r_version` read from the lockfile). Inputs
that contain shell metacharacters, newlines, or do not match the
documented format raise an explicit error at function entry,
rather than silently producing a malformed Dockerfile or one that
executes attacker-controlled commands at `docker build` time.
This closes the post-#106 audit follow-up for all five sites
surfaced by Copilot review.

## New features

- `dock$ARG()` and the internal `add_arg()` helper gain a `default`
Expand All @@ -37,10 +51,13 @@
via `--build-arg GITHUB_PAT=$GITHUB_PAT`), or `"secret"` to use
BuildKit secret mounts (the PAT is never persisted in image
metadata; recommended for published images). Closes #18.
- `dock_from_renv()` gains a `renv_paths_cache` parameter (default
`/root/.cache/R/renv`) used as the build-arg default, the propagated
`ENV` value and the cache mount target. Users can override the renv
cache location at image build time with
- `dock_from_renv()` gains a `renv_paths_cache` parameter that
controls the `RENV_PATHS_CACHE` build-arg default, the propagated
`ENV` value, and the cache mount target. When `NULL` (the
default), the path is auto-derived from `user`:
`/root/.cache/R/renv` for `user = NULL`, and
`/home/<user>/.cache/R/renv` otherwise. Users can override the
renv cache location at image build time with
`--build-arg RENV_PATHS_CACHE=...` without regenerating the
Dockerfile.
- `dock_from_desc()` gains a `strict_install` parameter (default
Expand Down
35 changes: 21 additions & 14 deletions R/dock_from_desc.R
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,29 @@ quote_not_na <- function(x){
#
#' @param path path to the DESCRIPTION file to use as an input.
#' @param FROM The FROM of the Dockerfile. Default is
#' FROM rocker/r-ver:`R.Version()$major`.`R.Version()$minor`.
#' @param AS The AS of the Dockerfile. Default it NULL.
#' `paste0("rocker/r-ver:", R.Version()$major, ".", R.Version()$minor)`.
#' Validated as a Docker image reference (alphanumerics, dot, slash,
#' dash, underscore, optional `:tag` and / or `@sha256:<hex>`); other
#' values raise an error to prevent shell-metacharacter injection
#' into the generated FROM directive.
#' @param AS The AS of the Dockerfile. Default it NULL. When non-NULL,
#' validated as a simple build-stage name (`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`).
#' @param sysreqs boolean. If TRUE, the Dockerfile will contain sysreq installation.
#' @param repos character. The URL(s) of the repositories to use for `options("repos")`.
#' @param repos character. The URL(s) of the repositories to use for
#' `options("repos")`. Each value must look like an http(s) URL (no
#' quotes, spaces or newlines); each name (when set) must be a simple
#' identifier (`^[A-Za-z][A-Za-z0-9._-]*$`). Other values raise an
#' error to prevent injection into the generated `echo "options(...)"`
#' shell command.
#' @param expand boolean. If `TRUE` each system requirement will have its own `RUN` line.
#' @param build_from_source boolean. If `TRUE` no tar.gz is created and
#' the Dockerfile directly mount the source folder.
#' @param update_tar_gz boolean. If `TRUE` and `build_from_source` is also `TRUE`,
#' an updated tar.gz is created.
#' @param extra_sysreqs character vector. Extra debian system requirements.
#' Will be installed with apt-get install.
#' Will be installed with apt-get install. Each entry must be a Debian
#' package name (`^[a-z0-9][a-z0-9.+-]+$`); other values raise an error
#' to prevent injection into the generated apt-get RUN.
#' @param github_pat character. How to provide a GitHub PAT to
#' `remotes::install_github()` for private dependency repositories.
#' One of `"none"` (default; the generated Dockerfile does not
Expand Down Expand Up @@ -103,16 +115,11 @@ dock_from_desc <- function(
strict_install = TRUE
) {
github_pat <- match.arg(github_pat)
if (
!is.logical(strict_install) ||
length(strict_install) != 1L ||
is.na(strict_install)
) {
stop(
"`strict_install` must be a single `TRUE` or `FALSE`, got: ",
deparse(strict_install)
)
}
.validate_scalar_logical(strict_install, "strict_install")
.validate_FROM(FROM)
.validate_AS(AS)
.validate_repos(repos)
.validate_extra_sysreqs(extra_sysreqs)
path <- fs::path_abs(path)

packages <- desc_get_deps(path)$package
Expand Down
45 changes: 39 additions & 6 deletions R/dock_from_renv.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,32 @@ pkg_sysreqs_mem <- memoise::memoise(

#' Create a Dockerfile from an `renv.lock` file
#'
#' @param lockfile Path to an `renv.lock` file to use as an input..
#' @param FROM Docker image to start FROM Default is FROM rocker/r-base
#' @param AS The AS of the Dockerfile. Default it `NULL`.
#' @param lockfile Path to an `renv.lock` file to use as an input.
#' The `basename(lockfile)` must be located at the docker build
#' context root at `docker build` time, because the generated
#' Dockerfile emits `COPY <basename(lockfile)> renv.lock`. Validated
#' as a single string whose basename contains only alphanumerics,
#' dots, underscores or hyphens (no spaces or shell metacharacters).
#' @param FROM Docker image to start FROM. Default is `"rocker/r-base"`.
#' Validated as a Docker image reference (alphanumerics, dot, slash,
#' dash, underscore, optional `:tag` and / or `@sha256:<hex>`); other
#' values raise an error to prevent shell-metacharacter injection
#' into the generated FROM directive.
#' @param AS The AS of the Dockerfile. Default is `NULL`. When non-NULL,
#' validated as a simple build-stage name (`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`).
#' @param distro - deprecated - only debian/ubuntu based images are supported
#' @param sysreqs boolean. If `TRUE`, the Dockerfile will contain sysreq installation.
#' @param expand boolean. If `TRUE` each system requirement will have its own `RUN` line.
#' @param repos character. The URL(s) of the repositories to use for `options("repos")`.
#' @param repos character. The URL(s) of the repositories to use for
#' `options("repos")`. Each value must look like an http(s) URL
#' (no quotes, spaces or newlines); each name (when set) must be a
#' simple identifier (`^[A-Za-z][A-Za-z0-9._-]*$`). Other values raise
#' an error to prevent injection into the generated `echo "options(...)"`
#' shell command.
#' @param extra_sysreqs character vector. Extra debian system requirements.
#' Will be installed with apt-get install.
#' Will be installed with apt-get install. Each entry must be a Debian
#' package name (`^[a-z0-9][a-z0-9.+-]+$`); other values raise an error
#' to prevent injection into the generated apt-get RUN.
#' @param renv_version character or `NULL`. The renv version to install.
#' The argument has three distinct modes, deliberately encoded with
#' the missing-vs-`NULL` distinction:
Expand All @@ -28,7 +45,12 @@ pkg_sysreqs_mem <- memoise::memoise(
#' version.
#' - a character string such as `"1.0.0"`: install that specific
#' version regardless of what the lockfile says.
#' @param use_pak boolean. If `TRUE` use pak to deal with dependencies during `renv::restore()`. FALSE by default
#'
#' When supplied as a string, validated as a version-like token
#' (`^[0-9]+(\.[0-9]+){0,3}([-.][a-zA-Z0-9]+)?$`).
#' @param use_pak boolean. If `TRUE` use pak to deal with dependencies
#' during `renv::restore()`. FALSE by default. Must be a single
#' `TRUE` or `FALSE` (no `NA`, no vector).
#' @param user Name of the user the runtime container drops privilege
#' to before the `renv::restore()` step (and therefore at runtime).
#' Default is `"rstudio"` so the generated image runs as a non-root
Expand Down Expand Up @@ -139,6 +161,16 @@ dock_from_renv <- function(
renv_paths_cache = NULL
) {
github_pat <- match.arg(github_pat)
.validate_lockfile(lockfile)
.validate_FROM(FROM)
.validate_AS(AS)
.validate_repos(repos)
.validate_extra_sysreqs(extra_sysreqs)
.validate_scalar_logical(use_pak, "use_pak")
Comment on lines 163 to +169
if (!missing(renv_version)) {
.validate_renv_version(renv_version)
}
Comment on lines +164 to +172
.validate_renv_paths_cache(renv_paths_cache)
if (!is.null(user)) {
# `user` is interpolated into shell commands (id, useradd, chown, USER).
# Reject anything that is not a strict POSIX username so a caller passing
Expand Down Expand Up @@ -173,6 +205,7 @@ dock_from_renv <- function(

# start the dockerfile
R_major_minor <- lock$R$Version
.validate_r_version(R_major_minor)
dock <- Dockerfile$new(
FROM = gen_base_image(
r_version = R_major_minor,
Expand Down
9 changes: 8 additions & 1 deletion R/gen_base_image.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ if (!is.null(distro)){
warning("the `distro` parameter is not used anymore, only debian/ubuntu based images are supported")
}

glue::glue("{FROM}:{r_version}")
# If the caller already pinned a tag (`:something`) or digest
# (`@sha256:...`), do not append `:r_version` on top of it -- that
# would yield an invalid image reference like `repo:tag:r_version`.
if (grepl("(:[^/]+|@sha256:[a-fA-F0-9]+)$", FROM)) {
glue::glue("{FROM}")
} else {
glue::glue("{FROM}:{r_version}")
}

}
Loading
Loading