From fa8969d419f7efa183799066df26f3b98ec342a3 Mon Sep 17 00:00:00 2001 From: thc202 Date: Thu, 27 Aug 2026 13:34:03 +0100 Subject: [PATCH 1/2] Stop earlier when active scan stopped Do not try to obtain a response if stopped. Interrupt the thread pool to allow other parts of the code to stop early as well and do not return new threads to not start more work. Signed-off-by: thc202 --- .../parosproxy/paros/common/ThreadPool.java | 18 ++++ .../paros/core/scanner/HostProcess.java | 6 ++ .../parosproxy/paros/core/scanner/Util.java | 1 + .../paros/common/ThreadPoolUnitTest.java | 94 +++++++++++++++++++ .../paros/core/scanner/UtilUnitTest.java | 10 ++ 5 files changed, 129 insertions(+) create mode 100644 zap/src/test/java/org/parosproxy/paros/common/ThreadPoolUnitTest.java diff --git a/zap/src/main/java/org/parosproxy/paros/common/ThreadPool.java b/zap/src/main/java/org/parosproxy/paros/common/ThreadPool.java index 532765ef6d2..3878b1ccd8b 100644 --- a/zap/src/main/java/org/parosproxy/paros/common/ThreadPool.java +++ b/zap/src/main/java/org/parosproxy/paros/common/ThreadPool.java @@ -24,10 +24,14 @@ // ZAP: 2019/06/05 Normalise format/style. package org.parosproxy.paros.common; +import java.util.Objects; +import java.util.stream.Stream; + public class ThreadPool { private Thread[] pool = null; private final String threadsBaseName; + private boolean interrupted; public ThreadPool(int maxThreadCount) { this(maxThreadCount, null); @@ -46,6 +50,9 @@ public ThreadPool(int maxThreadCount, String threadsBaseName) { * if none available. */ public synchronized Thread getFreeThreadAndRun(Runnable runnable) { + if (interrupted) { + return null; + } for (int i = 0; i < pool.length; i++) { if (pool[i] == null || !pool[i].isAlive()) { @@ -69,14 +76,19 @@ public synchronized Thread getFreeThreadAndRun(Runnable runnable) { * @param waitInMillis the number of milliseconds to wait for the threads */ public void waitAllThreadComplete(int waitInMillis) { + boolean waitInterrupted = false; for (int i = 0; i < pool.length; i++) { if (pool[i] != null && pool[i].isAlive()) { try { pool[i].join(waitInMillis); } catch (InterruptedException e) { + waitInterrupted = true; } } } + if (waitInterrupted) { + Thread.currentThread().interrupt(); + } } public boolean isAllThreadComplete() { @@ -87,4 +99,10 @@ public boolean isAllThreadComplete() { } return true; } + + public synchronized void interrupt() { + interrupted = true; + + Stream.of(pool).filter(Objects::nonNull).forEach(Thread::interrupt); + } } diff --git a/zap/src/main/java/org/parosproxy/paros/core/scanner/HostProcess.java b/zap/src/main/java/org/parosproxy/paros/core/scanner/HostProcess.java index 84b2ce19117..3d99979d0bd 100644 --- a/zap/src/main/java/org/parosproxy/paros/core/scanner/HostProcess.java +++ b/zap/src/main/java/org/parosproxy/paros/core/scanner/HostProcess.java @@ -347,6 +347,8 @@ public List getStartNodes() { public void stop() { isStop = true; getAnalyser().stop(); + + threadPool.interrupt(); } /** Main execution method */ @@ -715,6 +717,10 @@ private static void applyDeprecatedProperties(Plugin source, Plugin dest) { } private boolean obtainResponse(HistoryReference hRef, HttpMessage message) { + if (isStop()) { + return false; + } + try { getHttpSender().sendAndReceive(message); notifyNewMessage(message); diff --git a/zap/src/main/java/org/parosproxy/paros/core/scanner/Util.java b/zap/src/main/java/org/parosproxy/paros/core/scanner/Util.java index 5f22d3c71e3..d0825efe0b4 100644 --- a/zap/src/main/java/org/parosproxy/paros/core/scanner/Util.java +++ b/zap/src/main/java/org/parosproxy/paros/core/scanner/Util.java @@ -27,6 +27,7 @@ static void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } } diff --git a/zap/src/test/java/org/parosproxy/paros/common/ThreadPoolUnitTest.java b/zap/src/test/java/org/parosproxy/paros/common/ThreadPoolUnitTest.java new file mode 100644 index 00000000000..352888cbf41 --- /dev/null +++ b/zap/src/test/java/org/parosproxy/paros/common/ThreadPoolUnitTest.java @@ -0,0 +1,94 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2026 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.parosproxy.paros.common; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +class ThreadPoolUnitTest { + + @Test + void shouldInterruptRunningThreads() throws Exception { + // Given + ThreadPool pool = new ThreadPool(1); + CountDownLatch started = new CountDownLatch(1); + AtomicBoolean wasInterrupted = new AtomicBoolean(); + Thread t = + pool.getFreeThreadAndRun( + () -> { + started.countDown(); + try { + Thread.sleep(Long.MAX_VALUE); + } catch (InterruptedException e) { + wasInterrupted.set(true); + } + }); + started.await(); + // When + pool.interrupt(); + t.join(2000); + // Then + assertThat(wasInterrupted.get(), is(true)); + } + + @Test + void shouldNotReturnThreadAfterInterrupt() { + // Given + ThreadPool pool = new ThreadPool(2); + pool.interrupt(); + // When + Thread t = pool.getFreeThreadAndRun(() -> {}); + // Then + assertThat(t, is(nullValue())); + } + + @Test + void shouldRestoreInterruptedStateAfterWaitAllThreadComplete() throws Exception { + // Given + ThreadPool pool = new ThreadPool(1); + CountDownLatch started = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + pool.getFreeThreadAndRun( + () -> { + started.countDown(); + try { + release.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + started.await(); + Thread.currentThread().interrupt(); + // When + try { + pool.waitAllThreadComplete(5000); + // Then + assertThat(Thread.currentThread().isInterrupted(), is(true)); + } finally { + Thread.interrupted(); + release.countDown(); + } + } +} diff --git a/zap/src/test/java/org/parosproxy/paros/core/scanner/UtilUnitTest.java b/zap/src/test/java/org/parosproxy/paros/core/scanner/UtilUnitTest.java index e65c045235c..0b3845c0787 100644 --- a/zap/src/test/java/org/parosproxy/paros/core/scanner/UtilUnitTest.java +++ b/zap/src/test/java/org/parosproxy/paros/core/scanner/UtilUnitTest.java @@ -27,6 +27,16 @@ class UtilUnitTest { + @Test + void shouldRestoreInterruptedStateWhenSleepIsInterrupted() { + // Given + Thread.currentThread().interrupt(); + // When + Util.sleep(10); + // Then + assertThat(Thread.interrupted(), is(true)); + } + @Test void shouldPauseForGivenDuration() { // Given From 25f6da6b97000723438866c29f9584d6bf420f1d Mon Sep 17 00:00:00 2001 From: thc202 Date: Thu, 27 Aug 2026 11:40:22 +0100 Subject: [PATCH 2/2] Skip user authentication when interrupted Do not try to authenticate when interrupted to not take more time to terminate/return. Signed-off-by: thc202 --- .../main/java/org/zaproxy/zap/users/User.java | 15 ++++++ .../org/zaproxy/zap/users/UserUnitTest.java | 54 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/zap/src/main/java/org/zaproxy/zap/users/User.java b/zap/src/main/java/org/zaproxy/zap/users/User.java index b7516e78a3a..99b43184ce7 100644 --- a/zap/src/main/java/org/zaproxy/zap/users/User.java +++ b/zap/src/main/java/org/zaproxy/zap/users/User.java @@ -163,10 +163,17 @@ public int getId() { * @param message the message */ public void processMessageToMatchUser(HttpMessage message) { + if (interrupted()) { + return; + } + // If the user is not yet authenticated, authenticate now // Make sure there are no simultaneous authentications for the same user synchronized (this) { if (this.requiresAuthentication()) { + if (interrupted()) { + return; + } this.authenticate(); if (this.requiresAuthentication()) { LOGGER.info("Authentication failed for user: {}", name); @@ -177,6 +184,14 @@ public void processMessageToMatchUser(HttpMessage message) { processMessageToMatchAuthenticatedSession(message); } + private boolean interrupted() { + if (Thread.currentThread().isInterrupted()) { + LOGGER.debug("Skipping {} authentication due to interruption.", this.name); + return true; + } + return false; + } + /** * Modifies a message so its Request Header/Body matches the web session corresponding to this * user. diff --git a/zap/src/test/java/org/zaproxy/zap/users/UserUnitTest.java b/zap/src/test/java/org/zaproxy/zap/users/UserUnitTest.java index 1deab3374d2..fd446840b17 100644 --- a/zap/src/test/java/org/zaproxy/zap/users/UserUnitTest.java +++ b/zap/src/test/java/org/zaproxy/zap/users/UserUnitTest.java @@ -32,11 +32,13 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.concurrent.CountDownLatch; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -222,6 +224,58 @@ void shouldNotAuthenticateIfNotRequired() { verify(user, never()).authenticate(); } + @Test + void shouldNotAuthenticateWhenInterruptedBeforeCall() { + // Given + User user = spy(new User(CONTEXT_ID, USER_NAME)); + Thread.currentThread().interrupt(); + // When + try { + user.processMessageToMatchUser(mock()); + } finally { + Thread.interrupted(); + } + // Then + verify(user, never()).authenticate(); + } + + @Test + void shouldNotAuthenticateWhenInterruptedWhileWaitingForLock() throws Exception { + // Given + User user = spy(new User(CONTEXT_ID, USER_NAME)); + doReturn(true).when(user).requiresAuthentication(); + + CountDownLatch lockAcquired = new CountDownLatch(1); + CountDownLatch releaseLock = new CountDownLatch(1); + + Thread lockHolder = + new Thread( + () -> { + synchronized (user) { + lockAcquired.countDown(); + try { + releaseLock.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + lockHolder.start(); + lockAcquired.await(); + + Thread caller = new Thread(() -> user.processMessageToMatchUser(mock())); + caller.start(); + while (caller.getState() != Thread.State.BLOCKED) { + Thread.sleep(50); + } + caller.interrupt(); + releaseLock.countDown(); + caller.join(); + + // Then + verify(user, never()).authenticate(); + } + @Test void shouldNotRequireAuthenticationAfterAuthentication() { // Given