Skip to content
Closed
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
**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 weak regex validation in readline inputs
**Vulnerability:** Weak regex `^[0-9]+$` on interactive inputs allowed arbitrarily large integers which R coerces to `NA` when converting via `as.integer()`.
**Learning:** This `NA` coercion in subsequent `if` condition checks causes Unhandled Exception crashes and constitutes a Denial of Service (DoS) vulnerability. Bounded exact-match regex like `^[12]$` must be used for limited choices.
**Prevention:** Always use bounded exact-match regex validations when accepting user inputs intended for exact menu choices.
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
75 changes: 75 additions & 0 deletions tests/testthat/test-autoFIPC.R
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,78 @@ test_that("autoFIPC validates input types securely", {
"Security Error: tryEM must be a single non-NA logical value"
)
})

library(mockery)
test_that("interactive inputs are securely validated", {
mock_readline <- mockery::mock("3", "3", "3", "3", "1", "3", "3", "3", "3", "1", "3", "3", "3", "3", "1")
mockery::stub(aFIPC::autoFIPC, "readline", mock_readline)
# simulate interactive session
mockery::stub(aFIPC::autoFIPC, "interactive", TRUE)

# Check validation limits failure when 3 is supplied repeatedly
expect_error(
aFIPC::autoFIPC(
newformXData = data.frame(A=1),
oldformYData = data.frame(A=2),
newformCommonItemNames = c('A'),
oldformCommonItemNames = c('A'),
confirmCommonItems = NULL
),
"Too many invalid common item confirmation attempts"
)
})

test_that("interactive inputs are securely validated for 3PL prior questions", {
skip_if_not_installed("mirt")

set.seed(123)
old_data <- as.data.frame(matrix(sample(0:1, 100, replace=TRUE), ncol=5))
new_data <- as.data.frame(matrix(sample(0:1, 100, replace=TRUE), ncol=5))
names(old_data) <- paste0("I", 1:5)
names(new_data) <- paste0("I", 1:5)

mock_readline <- mockery::mock("3", "3", "3")
mockery::stub(aFIPC::autoFIPC, "readline", mock_readline)
mockery::stub(aFIPC::autoFIPC, "interactive", TRUE)

expect_error(
aFIPC::autoFIPC(
newformXData = new_data,
oldformYData = old_data,
newformCommonItemNames = c('I1'),
oldformCommonItemNames = c('I1'),
confirmCommonItems = TRUE,
itemtype = "3PL"
),
"Too many invalid oldform BILOG prior attempts"
)
})

test_that("interactive inputs are securely validated for 3PL prior questions newform", {
skip_if_not_installed("mirt")

set.seed(123)
old_data <- as.data.frame(matrix(sample(0:1, 100, replace=TRUE), ncol=5))
new_data <- as.data.frame(matrix(sample(0:1, 100, replace=TRUE), ncol=5))
names(old_data) <- paste0("I", 1:5)
names(new_data) <- paste0("I", 1:5)

# Suppress the mirt 1 cycle warning during the mock model generation
mock_old_model <- suppressWarnings(mirt::mirt(old_data, 1, itemtype = '3PL', technical = list(NCYCLES = 1), verbose = FALSE))

mock_readline <- mockery::mock("3", "3", "3")
mockery::stub(aFIPC::autoFIPC, "readline", mock_readline)
mockery::stub(aFIPC::autoFIPC, "interactive", TRUE)

expect_error(
aFIPC::autoFIPC(
newformXData = new_data,
oldformYData = mock_old_model,
newformCommonItemNames = c('I1'),
oldformCommonItemNames = c('I1'),
confirmCommonItems = TRUE,
itemtype = "3PL"
),
"Too many invalid newform BILOG prior attempts"
)
})
Loading