Skip to content

fix: scan can no longer hang indefinitely past its configured timeout#79

Merged
GovindarajanL merged 1 commit into
OWASP:mainfrom
GovindarajanL:fix/74-scan-timeout-and-concurrency
Jul 25, 2026
Merged

fix: scan can no longer hang indefinitely past its configured timeout#79
GovindarajanL merged 1 commit into
OWASP:mainfrom
GovindarajanL:fix/74-scan-timeout-and-concurrency

Conversation

@GovindarajanL

Copy link
Copy Markdown
Collaborator

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) 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. --threads was cosmetic. -t/--threads (documented as "number of concurrent threads, default 10") was parsed, validated, and printed to the console, but Scanner.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

  • 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.

Verification

  • After the concurrency fix: the exact crAPI scan that previously hung for hours now completes cleanly — 98 findings, no timeout, no manual kill.
  • 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 indefinitely.
  • New regression test: a mock server that tracks peak concurrent in-flight requests, asserting the peak never exceeds the configured --threads value (and that concurrency is actually exercised, not incidentally serial).
  • Full suite passes (241 tests).

Test plan

  • mvn test passes
  • Re-ran the authenticated crAPI scan (the exact original repro) with the fixed jar — completes cleanly with 98 findings, no hang

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>
@GovindarajanL
GovindarajanL merged commit 7949047 into OWASP:main Jul 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scan can hang indefinitely past the configured --timeout, requiring a manual kill

1 participant