Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions zap/src/main/java/org/parosproxy/paros/common/ThreadPool.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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()) {
Expand All @@ -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() {
Expand All @@ -87,4 +99,10 @@ public boolean isAllThreadComplete() {
}
return true;
}

public synchronized void interrupt() {
interrupted = true;

Stream.of(pool).filter(Objects::nonNull).forEach(Thread::interrupt);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@ public List<StructuralNode> getStartNodes() {
public void stop() {
isStop = true;
getAnalyser().stop();

threadPool.interrupt();
}

/** Main execution method */
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
15 changes: 15 additions & 0 deletions zap/src/main/java/org/zaproxy/zap/users/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions zap/src/test/java/org/zaproxy/zap/users/UserUnitTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading