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
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-08-01 - Fix integer coercion DoS vulnerability in readline prompts
**Vulnerability:** Weak regex `^[0-9]+$` on interactive `readline()` inputs allowed excessively large strings (e.g. "9999999999") to pass validation, which when coerced by `as.integer()` returned `NA`. This caused unhandled exceptions in subsequent `if` conditions, leading to unexpected crashes (Denial of Service).
**Learning:** Base `as.integer()` silently coerces out-of-bounds numeric strings to `NA` with a warning, bypassing simple digit-only regex checks.
**Prevention:** Use strictly bounded exact-match regex (like `^[12]$`) when validating interactive menu choices to guarantee safe integer coercion and prevent runtime crashes.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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: testthat (>= 3.0.0), mockery
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
69 changes: 69 additions & 0 deletions tests/testthat/test-sentinel-validation.R
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,72 @@ test_that("autoFIPC validates boolean flags for newformBILOGprior, oldformBILOGp
"Security Error: confirmCommonItems must be a single non-NA logical value or NULL"
)
})

test_that("autoFIPC interactive prompts prevent NA coercion crashes and DoS vulnerabilities from large integer inputs", {

# verify oldformBILOGprior validation protects against large integer inputs

mockery::stub(aFIPC::autoFIPC, 'interactive', function(...) TRUE)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mockery is not in DESCRIPTION Suggests (only testthat >= 3.0.0). R CMD check will fail on mockery::stub unless the runner happens to have a leftover install. Do not add mockery for this slice β€” #249 already mocks interactive / readline with testthat::local_mocked_bindings(..., .package = "aFIPC") after adding NULL namespace bindings.

mockery::stub(aFIPC::autoFIPC, 'readline', function(...) "9999999999")

expect_error(
aFIPC::autoFIPC(
newformXData = data.frame(A=1),
oldformYData = data.frame(A=2),
newformCommonItemNames = c('A'),
oldformCommonItemNames = c('A'),
confirmCommonItems = TRUE,
itemtype = '3PL',
tryFitwholeNewItems = FALSE,
tryFitwholeOldItems = FALSE,
checkIPD = FALSE
),
"Too many invalid oldform BILOG prior attempts"
)

# verify newformBILOGprior validation protects against large integer inputs
mockery::stub(aFIPC::autoFIPC, 'interactive', function(...) TRUE)
mockery::stub(aFIPC::autoFIPC, 'readline', function(...) "9999999999")

# We need oldformYData to be a model or at least have multiple categories so it bypasses failure during mirt fitting,
# or we can mock mirt::mirt so it returns a dummy model to reach newformBILOGprior check.
mod <- new("SingleGroupClass")
mod@OptimInfo$converged <- TRUE
mod@OptimInfo$secondordertest <- TRUE
mod@Data$data <- data.frame(A=c(1,0))
mockery::stub(aFIPC::autoFIPC, 'mirt::mirt', function(...) mod)
Comment on lines +67 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case does not reach checknewformBILOGprior().

  1. new("SingleGroupClass") plus slot writes is not a fitted model. isRealMirtModel() requires mirt::mod2values() to succeed; a dummy S4 object is rejected if used as data, and here the call still passes a 1-column data.frame.
  2. mockery::stub(..., "mirt::mirt", ...) does not intercept the mirt::mirt() calls in R/aFIPC.R. Old-form estimation on 1–2 rows will fail, then surveyFA, then Estimation failed. Please check test quality. β€” not Too many invalid newform BILOG prior attempts.

#249 reaches this prompt by passing a fitted 2PL mirt object as oldformYData and mocking only interactive / readline.

mockery::stub(aFIPC::autoFIPC, 'surveyFA', function(...) mod)

expect_error(
aFIPC::autoFIPC(
newformXData = data.frame(A=1),
oldformYData = data.frame(A=c(1,0)),
newformCommonItemNames = c('A'),
oldformCommonItemNames = c('A'),
confirmCommonItems = TRUE,
oldformBILOGprior = TRUE,
itemtype = '3PL',
tryFitwholeNewItems = FALSE,
tryFitwholeOldItems = FALSE,
checkIPD = FALSE
),
"Too many invalid newform BILOG prior attempts"
)

# verify confirmCommonItems validation protects against large integer inputs
mockery::stub(aFIPC::autoFIPC, 'interactive', function(...) TRUE)
mockery::stub(aFIPC::autoFIPC, 'readline', function(...) "9999999999")
expect_error(
aFIPC::autoFIPC(
newformXData = data.frame(A=1),
oldformYData = data.frame(A=2),
newformCommonItemNames = c('A'),
oldformCommonItemNames = c('A'),
itemtype = '3PL',
tryFitwholeNewItems = FALSE,
tryFitwholeOldItems = FALSE,
checkIPD = FALSE
),
"Too many invalid common item confirmation attempts"
)
})
Loading