From 927346b96d712e427bdc89dba84ceef352fa5e3d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:50:42 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=EB=B3=B4=EC=95=88=20=EA=B0=95=ED=99=94:=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=EB=9E=99=ED=8B=B0=EB=B8=8C=20=ED=94=84?= =?UTF-8?q?=EB=A1=AC=ED=94=84=ED=8A=B8=EC=9D=98=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=97=84?= =?UTF-8?q?=EA=B2=A9=ED=95=9C=20=EC=A0=95=EA=B7=9C=EC=8B=9D=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `readline()` 입력 검증시 사용된 취약한 정규식 `^[0-9]+$`을 `^[12]$`로 수정하여 메뉴 선택지에 없는 임의의 큰 숫자가 입력되는 것을 방지함. - `as.integer()` 변환 시 R의 32비트 정수 한계를 초과하는 값이 입력되어 발생하는 `NA` 강제 변환 및 후속 프로세스 오류(크래시)를 예방함. - 관련된 보안 학습 내용을 `.jules/sentinel.md` 저널에 기록함. --- .jules/sentinel.md | 5 +++++ R/aFIPC.R | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a8207a48..73f5e88f 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. + +## 2026-09-08 - Integer Coercion Vulnerability in Interactive Prompts +**Vulnerability:** Weak regex `^[0-9]+$` for `readline()` validation allowed arbitrarily large numbers, which coerced to `NA` when converted to 32-bit integers via `as.integer()`. +**Learning:** In R, integers have a strict 32-bit limit. Allowing unbounded numeric input for menu selections creates an input validation bypass that crashes downstream logic. +**Prevention:** Always use strictly bounded exact-match regex (e.g., `^[12]$`) for menu selections instead of generic numeric matching. diff --git a/R/aFIPC.R b/R/aFIPC.R index 62546519..918e19b1 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)) } } From e9210c6cb616603ac2c9f5388264a4845540afa2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 01:01:02 +0900 Subject: [PATCH 2/3] test: centralize bounded interactive choices --- .jules/sentinel.md | 5 -- R/aFIPC.R | 57 ++++++++++++----------- tests/testthat/test-sentinel-validation.R | 38 +++++++++++++++ 3 files changed, 68 insertions(+), 32 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 73f5e88f..a8207a48 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,8 +2,3 @@ **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. - -## 2026-09-08 - Integer Coercion Vulnerability in Interactive Prompts -**Vulnerability:** Weak regex `^[0-9]+$` for `readline()` validation allowed arbitrarily large numbers, which coerced to `NA` when converted to 32-bit integers via `as.integer()`. -**Learning:** In R, integers have a strict 32-bit limit. Allowing unbounded numeric input for menu selections creates an input validation bypass that crashes downstream logic. -**Prevention:** Always use strictly bounded exact-match regex (e.g., `^[12]$`) for menu selections instead of generic numeric matching. diff --git a/R/aFIPC.R b/R/aFIPC.R index 918e19b1..7c98b7d8 100644 --- a/R/aFIPC.R +++ b/R/aFIPC.R @@ -1,3 +1,21 @@ +#' Read a bounded binary menu choice +#' +#' @param prompt Prompt passed to the input reader. +#' @param error_message Error raised after three invalid responses. +#' @param read Input reader compatible with [readline()]. +#' @return Integer 1 or 2. +#' @keywords internal +#' @noRd +.read_binary_choice <- function(prompt, error_message, read = readline) { + for (attempt in seq_len(3)) { + value <- read(prompt = prompt) + if (value %in% c("1", "2")) { + return(as.integer(value)) + } + } + stop(error_message, call. = FALSE) +} + #' automated fixed item parameter linking #' #' @import mirt @@ -139,13 +157,10 @@ autoFIPC <- 'set confirmCommonItems = TRUE to accept the supplied pairs.' ) } - for (attempt in seq_len(3)) { - n <- readline(prompt = "Is it correct? (1: Yes 2: No) : ") - if (grepl("^[12]$", n)) { - return(as.integer(n)) - } - } - stop("Too many invalid common item confirmation attempts") + .read_binary_choice( + prompt = "Is it correct? (1: Yes 2: No) : ", + error_message = "Too many invalid common item confirmation attempts" + ) } confirm <- checkCorrect() if (confirm != 1) { @@ -166,16 +181,10 @@ autoFIPC <- if (itemtype == '3PL' && length(oldformBILOGprior) == 0) { checkoldformBILOGprior <- function() { if (!interactive()) stop("Interactive session required for oldform BILOG prior") - for (attempt in seq_len(3)) { - n <- - readline( - prompt = "Do you want to use default BILOG-MG priors for oldform Data? (1: Yes 2: No) : " - ) - if (grepl("^[12]$", n)) { - return(as.integer(n)) - } - } - stop("Too many invalid oldform BILOG prior attempts") + .read_binary_choice( + prompt = "Do you want to use default BILOG-MG priors for oldform Data? (1: Yes 2: No) : ", + error_message = "Too many invalid oldform BILOG prior attempts" + ) } oldformBILOGprior <- checkoldformBILOGprior() if (oldformBILOGprior == 1) { @@ -385,16 +394,10 @@ autoFIPC <- if (itemtype == '3PL' && length(newformBILOGprior) == 0) { checknewformBILOGprior <- function() { if (!interactive()) stop("Interactive session required for newform BILOG prior") - for (attempt in seq_len(3)) { - n <- - readline( - prompt = "Do you want to use default BILOG-MG priors for newform Data? (1: Yes 2: No) : " - ) - if (grepl("^[12]$", n)) { - return(as.integer(n)) - } - } - stop("Too many invalid newform BILOG prior attempts") + .read_binary_choice( + prompt = "Do you want to use default BILOG-MG priors for newform Data? (1: Yes 2: No) : ", + error_message = "Too many invalid newform BILOG prior attempts" + ) } newformBILOGprior <- checknewformBILOGprior() if (newformBILOGprior == 1) { diff --git a/tests/testthat/test-sentinel-validation.R b/tests/testthat/test-sentinel-validation.R index 900f0ee3..e0bd3cb4 100644 --- a/tests/testthat/test-sentinel-validation.R +++ b/tests/testthat/test-sentinel-validation.R @@ -35,3 +35,41 @@ test_that("autoFIPC validates boolean flags for newformBILOGprior, oldformBILOGp "Security Error: confirmCommonItems must be a single non-NA logical value or NULL" ) }) + + +test_that("binary menu choice accepts only exact documented values", { + make_reader <- function(values) { + force(values) + function(prompt) { + value <- values[[1]] + values <<- values[-1] + value + } + } + + expect_identical( + aFIPC:::.read_binary_choice("prompt", "invalid", make_reader("1")), + 1L + ) + expect_identical( + aFIPC:::.read_binary_choice("prompt", "invalid", make_reader("2")), + 2L + ) + expect_identical( + aFIPC:::.read_binary_choice( + "prompt", + "invalid", + make_reader(c("0", "3", "1")) + ), + 1L + ) + expect_error( + aFIPC:::.read_binary_choice( + "prompt", + "invalid", + make_reader(c("12", "2147483648", " 1")) + ), + "invalid", + fixed = TRUE + ) +}) From 1a5b5a780bf35238dabe1a57ef01f7c2d79ab62e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 06:05:08 +0900 Subject: [PATCH 3/3] test(input): carry bounded-choice regression corpus forward --- tests/testthat/test-sentinel-validation.R | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/testthat/test-sentinel-validation.R b/tests/testthat/test-sentinel-validation.R index e0bd3cb4..afe72ceb 100644 --- a/tests/testthat/test-sentinel-validation.R +++ b/tests/testthat/test-sentinel-validation.R @@ -72,4 +72,17 @@ test_that("binary menu choice accepts only exact documented values", { "invalid", fixed = TRUE ) + + for (value in c("3", "10", "2147483648", "invalid", "")) { + expect_error( + aFIPC:::.read_binary_choice( + "prompt", + "invalid", + make_reader(rep(value, 3)) + ), + "invalid", + fixed = TRUE, + info = paste("unexpectedly accepted binary menu value", dQuote(value)) + ) + } })