diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a8207a4..430ae1c 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -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. diff --git a/R/aFIPC.R b/R/aFIPC.R index 6254651..918e19b 100644 --- a/R/aFIPC.R +++ b/R/aFIPC.R @@ -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)) } } @@ -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)) } } @@ -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)) } } diff --git a/tests/testthat/test-autoFIPC.R b/tests/testthat/test-autoFIPC.R index 13cecd9..8950053 100644 --- a/tests/testthat/test-autoFIPC.R +++ b/tests/testthat/test-autoFIPC.R @@ -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" + ) +})