Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .github/workflows/r.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
- name: Set up R package dependencies
uses: r-lib/actions/setup-r-dependencies@d3c5be51b12e724e68f33216ca3c148b66d5f0b6
with:
# Refresh stale qs2/RcppParallel TBB ABI artifacts as one cache generation.
cache-version: '2'
extra-packages: any::rcmdcheck
needs: check

Expand Down
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
**Vulnerability:** Unvalidated inputs passed to `if()` statements can cause process crashes (`condition has length > 1`) or unexpected coercion vulnerabilities.
**Learning:** In R, optional boolean parameters that default to `NULL` should be validated using explicit runtime type validation (e.g., `if (!is.null(flag) && (!is.logical(flag) || length(flag) != 1 || is.na(flag)))`).
**Prevention:** Always implement explicit runtime type validation for optional boolean parameters.
## 2024-07-16 - Fix readline integer coercion DoS vulnerability
**Vulnerability:** Weak regex `^[0-9]+$` for integer validation allows extremely large numeric strings, which coerce to `NA` via `as.integer()` and crash the R process when evaluated in `if` conditions.
**Learning:** When reading integer inputs via `readline()` in R, avoid weak regex validation. Large numbers coerce to `NA` breaking control flow logic.
**Prevention:** Use strictly bounded exact-match regex like `^[12]$` when prompting for specific integer options to prevent unexpected coercions and DoS vulnerabilities.
4 changes: 3 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ Description: Automates fixed item parameter linking for test linking under
the item response theory paradigm using mirt package estimates.
License: GPL-3 | file LICENSE
Imports: mirt, methods
Suggests: testthat (>= 3.0.0)
Suggests:
mockery,
testthat (>= 3.0.0)
Encoding: UTF-8
Config/testthat/edition: 3
Config/roxygen2/version: 8.0.0
6 changes: 3 additions & 3 deletions R/aFIPC.R
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ autoFIPC <-
}
for (attempt in seq_len(3)) {
n <- readline(prompt = "Is it correct? (1: Yes 2: No) : ")
if (grepl("^[0-9]+$", n)) {
if (grepl("^[12]$", n)) {
return(as.integer(n))
}
}
Expand Down Expand Up @@ -171,7 +171,7 @@ autoFIPC <-
readline(
prompt = "Do you want to use default BILOG-MG priors for oldform Data? (1: Yes 2: No) : "
)
if (grepl("^[0-9]+$", n)) {
if (grepl("^[12]$", n)) {
return(as.integer(n))
}
}
Expand Down Expand Up @@ -390,7 +390,7 @@ autoFIPC <-
readline(
prompt = "Do you want to use default BILOG-MG priors for newform Data? (1: Yes 2: No) : "
)
if (grepl("^[0-9]+$", n)) {
if (grepl("^[12]$", n)) {
return(as.integer(n))
}
}
Expand Down
86 changes: 86 additions & 0 deletions tests/testthat/test-sentinel-validation.R
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,89 @@ test_that("autoFIPC validates boolean flags for newformBILOGprior, oldformBILOGp
"Security Error: confirmCommonItems must be a single non-NA logical value or NULL"
)
})

test_that("interactive readline input bounded regex validation prevents DoS", {
mockery::stub(aFIPC::autoFIPC, 'interactive', TRUE)
my_readline <- function(...) '99999999999999999999'
mockery::stub(aFIPC::autoFIPC, 'readline', my_readline)

# This should error out from 'Too many invalid common item confirmation attempts'
# instead of failing due to NA coercion in if condition
expect_error(
aFIPC::autoFIPC(
newformXData = data.frame(A=1),
oldformYData = data.frame(A=2),
newformCommonItemNames = c('A'),
oldformCommonItemNames = c('A')
),
"Too many invalid common item confirmation attempts"
)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.


test_that("oldform BILOG prompt rejects repeated overlong integer input", {
mockery::stub(aFIPC::autoFIPC, "interactive", TRUE)
calls <- new.env(parent = emptyenv())
calls$count <- 0L
overlong_readline <- function(...) {
calls$count <- calls$count + 1L
"99999999999999999999"
}
mockery::stub(aFIPC::autoFIPC, "readline", overlong_readline)

expect_error(
aFIPC::autoFIPC(
newformXData = data.frame(A = 1),
oldformYData = data.frame(A = 2),
newformCommonItemNames = "A",
oldformCommonItemNames = "A",
newformBILOGprior = FALSE,
confirmCommonItems = TRUE
),
"Too many invalid oldform BILOG prior attempts"
)
expect_equal(calls$count, 3L)
})

test_that("newform BILOG prompt rejects repeated overlong integer input", {
skip_if_not_installed("mirt")
set.seed(20260815)
old_data <- as.data.frame(mirt::simdata(
a = matrix(rep(1, 4), ncol = 1),
d = c(-1, -0.3, 0.3, 1),
itemtype = rep("2PL", 4),
N = 200
))
names(old_data) <- LETTERS[1:4]
old_model <- mirt::mirt(
old_data,
1,
itemtype = "2PL",
SE = FALSE,
verbose = FALSE
)

mockery::stub(aFIPC::autoFIPC, "interactive", TRUE)
calls <- new.env(parent = emptyenv())
calls$count <- 0L
overlong_readline <- function(...) {
calls$count <- calls$count + 1L
"99999999999999999999"
}
mockery::stub(aFIPC::autoFIPC, "readline", overlong_readline)

expect_error(
aFIPC::autoFIPC(
newformXData = old_data,
oldformYData = old_model,
newformCommonItemNames = "A",
oldformCommonItemNames = "A",
itemtype = "3PL",
oldformBILOGprior = FALSE,
confirmCommonItems = TRUE,
checkIPD = FALSE
),
"Too many invalid newform BILOG prior attempts"
)
expect_equal(calls$count, 3L)
})
29 changes: 29 additions & 0 deletions tests/testthat/test-workflow-cache.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
test_that("R CMD check uses an active reviewed dependency cache generation", {
repository_root <- Sys.getenv(
"GITHUB_WORKSPACE",
unset = testthat::test_path("..", "..")
)
workflow_path <- file.path(repository_root, ".github", "workflows", "r.yml")
workflow_lines <- readLines(workflow_path, warn = FALSE)
dependency_step <- grep(
"r-lib/actions/setup-r-dependencies@",
workflow_lines,
fixed = TRUE
)
expect_length(dependency_step, 1L)
dependency_block <- workflow_lines[
dependency_step:min(dependency_step + 8L, length(workflow_lines))
]
active_cache_version_pattern <- paste0(
"^[[:space:]]*cache-version:[[:space:]]*",
"['\\\"]2['\\\"][[:space:]]*(#.*)?$"
)

expect_false(
grepl(active_cache_version_pattern, "# cache-version: '2'", perl = TRUE)
)
expect_true(
any(grepl(active_cache_version_pattern, dependency_block, perl = TRUE)),
info = "The TBB ABI cache refresh must remain an active workflow input"
)
})
Loading