Skip to content
15 changes: 5 additions & 10 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
## 2024-06-23 - μž¬κ·€μ  μž…λ ₯ κ²€μ¦μœΌλ‘œ μΈν•œ λŒ€ν™”ν˜• ν”„λ‘¬ν”„νŠΈ DoS 취약점
**Vulnerability:** λŒ€ν™”ν˜• ν”„λ‘¬ν”„νŠΈ ν•¨μˆ˜(`checkCorrect`, `checkoldformBILOGprior`, `checknewformBILOGprior`)μ—μ„œ μ‚¬μš©μžκ°€ μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ κ°’(예: μˆ«μžκ°€ μ•„λ‹Œ λ¬Έμžμ—΄)을 μž…λ ₯ν–ˆμ„ λ•Œ, 꼬리 μž¬κ·€(tail-recursive) λ°©μ‹μœΌλ‘œ 자기 μžμ‹ μ„ ν˜ΈμΆœν•˜λ„λ‘ κ΅¬ν˜„λ˜μ–΄ μžˆμ—ˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” R의 C μŠ€νƒ μ‚¬μ΄μ¦ˆ μ œν•œμœΌλ‘œ 인해 μŠ€νƒ 고갈(Stack Exhaustion)둜 μ΄μ–΄μ§€λŠ” DoS(μ„œλΉ„μŠ€ κ±°λΆ€) 취약점을 μ•ΌκΈ°ν•  수 있으며, `readline()`이 계속 빈 λ¬Έμžμ—΄μ„ λ°˜ν™˜ν•˜λŠ” λΉ„λŒ€ν™”ν˜•(non-interactive) CI/CD ν™˜κ²½μ—μ„œλŠ” λ¬΄ν•œ 루프λ₯Ό λ°œμƒμ‹œμΌ°μŠ΅λ‹ˆλ‹€.
**Learning:** μž…λ ₯ 검증 λ£¨ν”„λŠ” μž¬κ·€ ν˜ΈμΆœμ„ μ‚¬μš©ν•˜μ—¬ κ΅¬ν˜„ν•΄μ„œλŠ” μ•ˆ 되며, 특히 μžλ™ν™”λœ ν™˜κ²½μ—μ„œ μž…λ ₯이 제곡될 λ•Œ λ”μš± μ£Όμ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€. λΉ„λŒ€ν™”ν˜• μ„Έμ…˜μ—μ„œμ˜ νƒˆμΆœκ΅¬(escape hatch)κ°€ μ—†λŠ” μž¬κ·€μ  μž…λ ₯ λ£¨ν”„λŠ” λ¦¬μ†ŒμŠ€λ₯Ό μ¦‰μ‹œ κ³ κ°ˆμ‹œμΌœ μžλ™ν™”λœ ν…ŒμŠ€νŠΈ 및 CI ν™˜κ²½μ„ μ†μƒμ‹œν‚΅λ‹ˆλ‹€.
## 2024-07-02 - λͺ¨λΈ μΆ”μ • μž¬μ‹œλ„ 쀑 λ¬΄ν•œ 루프 DoS λ°©μ§€
**Vulnerability:** μ½”λ“œμ—μ„œ `while (!exists('model'))`κ³Ό `try(...)`λ₯Ό κ²°ν•©ν•˜μ—¬ λͺ¨λΈ μΆ”μ • μ‹€νŒ¨ μ‹œ 반볡적으둜 μž¬μ‹œλ„ν•˜λ„λ‘ κ΅¬ν˜„λ˜μ–΄ μžˆμ—ˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ `exists()`κ°€ 기본값인 `inherits = TRUE`둜 λ™μž‘ν•΄ μƒμœ„ ν™˜κ²½μ˜ 객체λ₯Ό μ°Έμ‘°ν•  μœ„ν—˜μ΄ μ‘΄μž¬ν–ˆμŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ 쑰건듀은 잘λͺ»λœ λ°μ΄ν„°λ‚˜ νŒŒλΌλ―Έν„°λ‘œ 인해 좔정이 μ§€μ†μ μœΌλ‘œ μ‹€νŒ¨ν•  경우 λ¬΄ν•œ 루프가 λ°œμƒν•˜μ—¬ ν”„λ‘œμ„ΈμŠ€κ°€ λ¬΄κΈ°ν•œ μ€‘λ‹¨λ˜λŠ” μ„œλΉ„μŠ€ κ±°λΆ€(DoS) 취약점을 μ•ΌκΈ°ν•©λ‹ˆλ‹€.
**Learning:** 루프 쑰건 검사 μ‹œ `try()` ν•¨μˆ˜λŠ” μ—λŸ¬λ₯Ό μ–΅μ œν•˜λ―€λ‘œ, `exists()` 쑰건이 λ§Œμ‘±λ˜μ§€ μ•ŠμœΌλ©΄ νƒˆμΆœ 쑰건 없이 루프가 λ¬΄ν•œνžˆ κ³„μ†λ©λ‹ˆλ‹€. `max_retries` μ œν•œ 및 `inherits = FALSE` 속성이 ν•„μš”ν•©λ‹ˆλ‹€.
**Prevention:**
1. μž…λ ₯ 검증 μ‹œ μž¬κ·€ 호좜 λŒ€μ‹  `repeat` 루프λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
2. μˆ˜λ™ μž…λ ₯을 μš”κ΅¬ν•˜κΈ° 전에 항상 `!interactive()`λ₯Ό μ‚¬μš©ν•˜μ—¬ ν˜„μž¬ μ„Έμ…˜μ΄ λŒ€ν™”ν˜•μΈμ§€ ν™•μΈν•˜κ³ , λΉ„λŒ€ν™”ν˜• μ„Έμ…˜μΌ κ²½μš°μ—λŠ” μ¦‰μ‹œ μ—λŸ¬λ₯Ό λ°œμƒμ‹œμΌœ μ•ˆμ „ν•˜κ²Œ μ‹€νŒ¨(fail securely)ν•˜λ„λ‘ μ²˜λ¦¬ν•©λ‹ˆλ‹€.
3. common item ν™•μΈμ²˜λŸΌ μΆ”μ • 결과의 기쀀척도와 true parameter μž¬ν˜„μ— 직접 영ν–₯을 μ£ΌλŠ” μž…λ ₯은 λΉ„λŒ€ν™”ν˜• ν™˜κ²½μ—μ„œ κΈ°λ³Έκ°’μœΌλ‘œ μžλ™ μŠΉμΈν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. μžλ™ν™”μ—μ„œλŠ” `confirmCommonItems = TRUE`처럼 λͺ…μ‹œμ  opt-in을 μš”κ΅¬ν•©λ‹ˆλ‹€.
4. λŒ€ν™”ν˜• μž¬μž…λ ₯ 루프도 λ¬΄ν•œ λ°˜λ³΅ν•˜μ§€ μ•Šλ„λ‘ μ œν•œλœ 횟수만 ν—ˆμš©ν•˜κ³ , 초과 μ‹œ λͺ…ν™•ν•œ μ—λŸ¬λ‘œ μ’…λ£Œν•©λ‹ˆλ‹€.
5. DoS μ™„ν™”λ₯Ό μœ„ν•΄ `return(1L)` 같은 κΈ°λ³Έ μŠΉμΈκ°’μ„ 넣을 λ•ŒλŠ” μΆ”μ • 기쀀척도, anchor/common item, true parameter μž¬ν˜„ 계약을 μš°νšŒν•˜μ§€ μ•ŠλŠ”μ§€ λ¨Όμ € κ²€μ¦ν•©λ‹ˆλ‹€.
6. Fail-secure μ—λŸ¬ λ©”μ‹œμ§€λŠ” ν…ŒμŠ€νŠΈμ˜ μΌλΆ€λ‘œ μ·¨κΈ‰ν•©λ‹ˆλ‹€. λ³΄μ•ˆ ν…ŒμŠ€νŠΈλŠ” μ‹€μ œ κ΅¬ν˜„ λ©”μ‹œμ§€μ™€ λ§žμ•„μ•Ό ν•˜λ©°, 였래된 `"Interactive prompt is not available"` 같은 별도 문ꡬλ₯Ό μƒˆλ‘œ λ§Œλ“€μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
7. Prompt DoS νšŒκ·€ ν…ŒμŠ€νŠΈλŠ” λͺ¨λΈ μΆ”μ • μ‹€νŒ¨μ— κΈ°λŒ€μ§€ 말고, common-item confirmation guard처럼 μ·¨μ•½ν•œ μž…λ ₯ κ²½κ³„μ—μ„œ λ°”λ‘œ λ°œμƒν•˜λŠ” fail-secure μ—λŸ¬λ₯Ό κ²€μ¦ν•©λ‹ˆλ‹€.
1. 잠재적으둜 μ‹€νŒ¨ν•  수 μžˆλŠ” μž‘μ—…(특히 `try()`λ₯Ό μ‚¬μš©ν•˜κ±°λ‚˜ μ—λŸ¬λ₯Ό μž‘λŠ” 경우)을 μž¬μ‹œλ„ν•˜λŠ” `while` λ£¨ν”„μ—λŠ” 항상 `max_retries` μΉ΄μš΄ν„°λ₯Ό κ΅¬ν˜„ν•˜μ—¬ λ¬΄ν•œ 루프λ₯Ό λ°©μ§€ν•©λ‹ˆλ‹€. 쀑볡을 λ°©μ§€ν•˜κΈ° μœ„ν•΄ λ‘œμ§μ„ λͺ¨λ“ˆν™”ν•˜κ±°λ‚˜ 쀑앙 μ§‘μ€‘ν™”ν•©λ‹ˆλ‹€.
2. `exists()`λ₯Ό μ‚¬μš©ν•˜μ—¬ λ³€μˆ˜ 쑴재 유무λ₯Ό 검사할 λ•ŒλŠ” 항상 `inherits = FALSE`λ₯Ό λͺ…μ‹œν•˜μ—¬ μƒμœ„ μŠ€μ½”ν”„μ— μ˜ν•œ 예기치 λͺ»ν•œ μŠ€ν‚΅μ„ λ°©μ§€ν•©λ‹ˆλ‹€.
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export(autoFIPC)
export(surveyFA)
import(mirt)
importFrom(stats,factanal)
importFrom("stats", "na.omit")
16 changes: 14 additions & 2 deletions R/aFIPC.R
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#' @param parameterOverwrite don't touch it
#' @param empiricalhist do you want to use empirical histogram method when tryEM = TRUE? default is FALSE
#' @param confirmCommonItems set TRUE to accept the supplied common-item pairs without an interactive prompt.
#' @param max_retries maximum retries limit for MHRM fallback.
#' @param ... Additional arguments reserved for future extensions.
#'
#' @return the model list of the base form, new form, linked form
Expand Down Expand Up @@ -45,6 +46,7 @@ autoFIPC <-
parameterOverwrite = F,
empiricalhist = F,
confirmCommonItems = NULL,
max_retries = 5,
...
) {
# print credits
Expand Down Expand Up @@ -216,7 +218,8 @@ autoFIPC <-
)

try(rm(oldFormModel))
while (!exists('oldFormModel')) {
retry_count <- 0
while (!exists('oldFormModel', inherits = FALSE) && retry_count < max_retries) {
try(
oldFormModel <-
mirt::mirt(
Expand All @@ -230,6 +233,10 @@ autoFIPC <-
GenRandomPars = F
)
)
retry_count <- retry_count + 1
}
if (!exists('oldFormModel', inherits = FALSE)) {
stop("\ucd5c\ub300\u0020\uc7ac\uc2dc\ub3c4\u0020\ud69f\uc218\u0020\ucd08\uacfc\u0020\ud6c4\u0020\ubaa8\ub378\u0020\ucd94\uc815\uc5d0\u0020\uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4\u002e\u0020\ub370\uc774\ud130\u0020\ubc0f\u0020\ud30c\ub77c\ubbf8\ud130\ub97c\u0020\ud655\uc778\ud558\uc2ed\uc2dc\uc624\u002e")
}
}
}
Expand Down Expand Up @@ -428,7 +435,8 @@ autoFIPC <-
)

try(rm(newFormModel))
while (!exists('newFormModel')) {
retry_count <- 0
while (!exists('newFormModel', inherits = FALSE) && retry_count < max_retries) {
try(
newFormModel <-
mirt::mirt(
Expand All @@ -442,6 +450,10 @@ autoFIPC <-
GenRandomPars = F
)
)
retry_count <- retry_count + 1
}
if (!exists('newFormModel', inherits = FALSE)) {
stop("\ucd5c\ub300\u0020\uc7ac\uc2dc\ub3c4\u0020\ud69f\uc218\u0020\ucd08\uacfc\u0020\ud6c4\u0020\ubaa8\ub378\u0020\ucd94\uc815\uc5d0\u0020\uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4\u002e\u0020\ub370\uc774\ud130\u0020\ubc0f\u0020\ud30c\ub77c\ubbf8\ud130\ub97c\u0020\ud655\uc778\ud558\uc2ed\uc2dc\uc624\u002e")
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions man/autoFIPC.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions tests/testthat/test-dos-prevention.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
test_that("while loops for model estimation limit retries to prevent DoS", {
# To test the fix safely, we don't need a full run of autoFIPC which is heavy.
# We just evaluate the logic from aFIPC.R directly via mock environment or simple mock function.

# A simple local function that simulates the logic inside autoFIPC to verify retry_count works
test_retry_loop <- function() {
max_retries <- 5
retry_count <- 0
# Simulate a loop where model doesn't exist
while (!exists('mockModel', inherits = FALSE) && retry_count < max_retries) {
try(
stop("simulated failure")
)
retry_count <- retry_count + 1
}
if (!exists('mockModel', inherits = FALSE)) {
stop("μ΅œλŒ€ μž¬μ‹œλ„ 횟수 초과 ν›„ λͺ¨λΈ 좔정에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. 데이터 및 νŒŒλΌλ―Έν„°λ₯Ό ν™•μΈν•˜μ‹­μ‹œμ˜€.")
}
}

expect_error(
test_retry_loop(),
"μ΅œλŒ€ μž¬μ‹œλ„ 횟수 초과 ν›„ λͺ¨λΈ 좔정에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. 데이터 및 νŒŒλΌλ―Έν„°λ₯Ό ν™•μΈν•˜μ‹­μ‹œμ˜€.",
fixed = TRUE
)
})
Loading