fix: scan can no longer hang indefinitely past its configured timeout#79
Merged
GovindarajanL merged 1 commit intoJul 25, 2026
Conversation
Two compounding bugs let a scan hang for hours with no report, no error, and no exit, requiring a manual kill: 1. Scanner.scan() used try-with-resources on the virtual-thread executor. ExecutorService.close() (the try-with-resources exit path) calls shutdown() -- which lets already-running tasks finish on their own -- then awaits termination in up to 24-hour increments, repeating until every task terminates. If a single task never completes, that wait can silently outlast the orTimeout() configured via --timeout by hours, defeating the entire point of a scan timeout. 2. -t/--threads (documented as "number of concurrent threads, default 10") was parsed, validated, and printed to the console, but never actually applied. Every endpoint x test-case combination fired as a fully unbounded concurrent virtual thread -- e.g. 44 endpoints x 12 test cases = 528 simultaneous requests -- which can overwhelm a target badly enough that requests stall well past any per-request HTTP timeout. Reproduced against a local OWASP crAPI stack with a valid auth token: progress stalled at 96.6% (510/528 tasks) for hours with zero further log output, well past the default 30-minute timeout. Required a manual process kill; no report was ever produced. Reproduced repeatedly even against a freshly recreated stack, ruling out server-side state as the sole cause. Fix: - Replace the try-with-resources executor with an explicit try/finally that calls shutdownNow() and bounds the wait to a single 10-second awaitTermination, guaranteeing scan() always returns with whatever findings were collected -- never hangs. - Add a Semaphore sized to config.getThreads() around each task's execution, so --threads actually bounds concurrent in-flight requests as documented. Verified: after the concurrency fix, the exact crAPI scan that previously hung for hours now completes cleanly (98 findings, no timeout, no manual kill needed). Before the concurrency fix landed, one intermediate run genuinely hit the 30-minute timeout and confirmed the shutdown-bounding fix works in isolation -- it logged the timeout warning, force-shut-down within ~11 seconds, and wrote a report with the findings collected so far, instead of hanging. Fixes OWASP#74 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #74
Summary
Two compounding bugs let a scan hang for hours with no report, no error, and no exit, requiring a manual kill.
1. Unbounded executor shutdown.
Scanner.scan()used try-with-resources on the virtual-thread executor.ExecutorService.close()(the try-with-resources exit path) callsshutdown()— which lets already-running tasks finish on their own — then awaits termination in up to 24-hour increments, repeating until every task terminates. If a single task never completes, that wait can silently outlast theorTimeout()configured via--timeoutby hours, defeating the entire point of a scan timeout.2.
--threadswas cosmetic.-t/--threads(documented as "number of concurrent threads, default 10") was parsed, validated, and printed to the console, butScanner.scan()never read it. Every endpoint × test-case combination fired as a fully unbounded concurrent virtual thread — e.g. 44 endpoints × 12 test cases = 528 simultaneous requests — which can overwhelm a target badly enough that requests stall well past any per-request HTTP timeout.Reproduction
Ran against a local OWASP crAPI docker-compose stack with a valid auth token. Progress stalled at 96.6% (510/528 tasks) for hours with zero further log output, well past the default 30-minute
--timeout. Required a manual process kill; no report was ever produced. Reproduced repeatedly even against a freshly recreated stack (ruling out server-side leftover state as the sole cause), until the concurrency bug (#2) was found and fixed.Fix
try/finallythat callsshutdownNow()and bounds the wait to a single 10-secondawaitTermination, guaranteeingscan()always returns with whatever findings were collected — never hangs.Semaphoresized toconfig.getThreads()around each task's execution, so--threadsactually bounds concurrent in-flight requests as documented.Verification
--threadsvalue (and that concurrency is actually exercised, not incidentally serial).Test plan
mvn testpasses