Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UNDERTOW-2413][UNDERTOW-2382] CVE-2024-5971 CVE-2024-3653 Backport bug fixes #1641

Merged
merged 3 commits into from
Jul 16, 2024
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
16 changes: 15 additions & 1 deletion core/src/main/java/io/undertow/Handlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -555,14 +555,28 @@ public static ResponseRateLimitingHandler responseRateLimitingHandler(HttpHandle
* Creates a handler that automatically learns which resources to push based on the referer header
*
* @param maxEntries The maximum number of entries to store
* @param maxAge The maximum age of the entries
* @param maxAge The maximum age of the entries (ms)
* @param next The next handler
* @return A caching push handler
*/
public static LearningPushHandler learningPushHandler(int maxEntries, int maxAge, HttpHandler next) {
return new LearningPushHandler(maxEntries, maxAge, next);
}

/**
* Creates a handler that automatically learns which resources to push based on the referer header
*
* @param maxPathEntries The maximum number of entries to store. Path->{1..n Push}
* @param maxPathAge The maximum age of the entries (ms)
* @param maxPushEntries The maximum number of push entries stored for given path
* @param maxPushAge The maximum age of push entry (ms)
* @param next The next handler
* @return A caching push handler
*/
public static LearningPushHandler learningPushHandler(final int maxPathEntries, final int maxPathAge, final int maxPushEntries, final int maxPushAge, final HttpHandler next) {
return new LearningPushHandler(maxPathEntries, maxPathAge, maxPushEntries, maxPushAge, next);
}

/**
* A handler that sets response code but continues the exchange so the servlet's
* error page can be returned.
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/io/undertow/protocols/ssl/SslConduit.java
Original file line number Diff line number Diff line change
Expand Up @@ -1001,13 +1001,18 @@ private synchronized long doWrap(ByteBuffer[] userBuffers, int off, int len) thr

private SSLEngineResult wrapAndFlip(ByteBuffer[] userBuffers, int off, int len) throws IOException {
SSLEngineResult result = null;
int totalConsumedBytes = 0;
while (result == null || (result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP
&& result.getStatus() != SSLEngineResult.Status.BUFFER_OVERFLOW && !engine.isInboundDone())) {
if (userBuffers == null) {
result = engine.wrap(EMPTY_BUFFER, wrappedData.getBuffer());
} else {
result = engine.wrap(userBuffers, off, len, wrappedData.getBuffer());
}
totalConsumedBytes += result.bytesConsumed();
}
if (totalConsumedBytes != result.bytesConsumed()) {
result = new SSLEngineResult(result.getStatus(), result.getHandshakeStatus(), totalConsumedBytes, result.bytesProduced());
}
wrappedData.getBuffer().flip();
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import io.undertow.util.Headers;
import io.undertow.util.Methods;

import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -47,20 +46,31 @@
public class LearningPushHandler implements HttpHandler {

private static final String SESSION_ATTRIBUTE = "io.undertow.PUSHED_RESOURCES";
private static final int DEFAULT_MAX_CACHE_ENTRIES = 1000;
private static final int DEFAULT_MAX_CACHE_AGE = -1;
private static final int DEFAULT_MAX_CACHE_ENTRIES = Integer.getInteger("io.undertow.handlers.learning-push.default-max-entries", 200);
private static final int DEFAULT_MAX_CACHE_AGE = Integer.getInteger("io.undertow.handlers.learning-push.default-max-age", LRUCache.MAX_AGE_NO_EXPIRY);

private final LRUCache<String, Map<String, PushedRequest>> cache;
private final LRUCache<String, LRUCache<String, PushedRequest>> cache;

private final HttpHandler next;
private int maxPushCacheEntries;
private int maxPushCacheAge;

public LearningPushHandler(final HttpHandler next) {
this(DEFAULT_MAX_CACHE_ENTRIES, DEFAULT_MAX_CACHE_AGE, next);
}

public LearningPushHandler(int maxEntries, int maxAge, HttpHandler next) {
public LearningPushHandler(int maxPathEtries, int maxPathAge, HttpHandler next) {
this.next = next;
cache = new LRUCache<>(maxEntries, maxAge);
this.maxPushCacheEntries = maxPathEtries;
this.maxPushCacheAge = maxPathAge;
cache = new LRUCache<>(maxPathEtries, maxPathAge);
}

public LearningPushHandler(int maxPathEtries, int maxPathAge, int maxPushEtries, int maxPushAge, HttpHandler next) {
this.next = next;
this.maxPushCacheEntries = maxPushEtries;
this.maxPushCacheAge = maxPushAge;
cache = new LRUCache<>(maxPathEtries, maxPathAge);
}

@Override
Expand Down Expand Up @@ -92,18 +102,21 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {

private void doPush(HttpServerExchange exchange, String fullPath) {
if (exchange.getConnection().isPushSupported()) {
Map<String, PushedRequest> toPush = cache.get(fullPath);
LRUCache<String, PushedRequest> toPush = cache.get(fullPath);
if (toPush != null) {
Session session = getSession(exchange);
if (session == null) {
return;
}
Map<String, Object> pushed = (Map<String, Object>) session.getAttribute(SESSION_ATTRIBUTE);
LRUCache<String, Object> pushed = (LRUCache<String, Object>) session.getAttribute(SESSION_ATTRIBUTE);
if (pushed == null) {
pushed = Collections.synchronizedMap(new HashMap<String, Object>());
pushed = new LRUCache<>(this.maxPushCacheEntries, this.maxPushCacheAge);
}
for (Map.Entry<String, PushedRequest> entry : toPush.entrySet()) {
PushedRequest request = entry.getValue();
for (String entryKey : toPush.keySet()) {
PushedRequest request = toPush.get(entryKey);
if(request == null) {
continue;
}
Object pushedKey = pushed.get(request.getPath());
boolean doPush = pushedKey == null;
if (!doPush) {
Expand All @@ -114,11 +127,12 @@ private void doPush(HttpServerExchange exchange, String fullPath) {
}
}
if (doPush) {
//pushResource will fill request headers.
exchange.getConnection().pushResource(request.getPath(), Methods.GET, request.getRequestHeaders());
if(request.getEtag() != null) {
pushed.put(request.getPath(), request.getEtag());
pushed.add(request.getPath(), request.getEtag());
} else {
pushed.put(request.getPath(), request.getLastModified());
pushed.add(request.getPath(), request.getLastModified());
}
}
}
Expand Down Expand Up @@ -166,16 +180,16 @@ public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener
lastModified = dt.getTime();
}
}
Map<String, PushedRequest> pushes = cache.get(referer);
LRUCache<String, PushedRequest> pushes = cache.get(referer);
if(pushes == null) {
synchronized (cache) {
pushes = cache.get(referer);
if(pushes == null) {
cache.add(referer, pushes = Collections.synchronizedMap(new HashMap<String, PushedRequest>()));
cache.add(referer, pushes = new LRUCache<String, LearningPushHandler.PushedRequest>(maxPushCacheEntries, maxPushCacheAge));
}
}
}
pushes.put(fullPath, new PushedRequest(new HeaderMap(), requestPath, etag, lastModified));
pushes.add(fullPath, new PushedRequest(new HeaderMap(), requestPath, etag, lastModified));
}

nextListener.proceed();
Expand Down Expand Up @@ -224,6 +238,8 @@ public Map<String, Class<?>> parameters() {
Map<String, Class<?>> params = new HashMap<>();
params.put("max-age", Integer.class);
params.put("max-entries", Integer.class);
params.put("max-push-age", Integer.class);
params.put("max-push-entries", Integer.class);
return params;
}

Expand All @@ -241,11 +257,12 @@ public String defaultParameter() {
public HandlerWrapper build(Map<String, Object> config) {
final int maxAge = config.containsKey("max-age") ? (Integer)config.get("max-age") : DEFAULT_MAX_CACHE_AGE;
final int maxEntries = config.containsKey("max-entries") ? (Integer)config.get("max-entries") : DEFAULT_MAX_CACHE_ENTRIES;

final int maxPushAge = config.containsKey("max-push-age") ? (Integer)config.get("max-push-age") : DEFAULT_MAX_CACHE_AGE;
final int maxPushEntries = config.containsKey("max-push-entries") ? (Integer)config.get("max-push-entries") : DEFAULT_MAX_CACHE_ENTRIES;
return new HandlerWrapper() {
@Override
public HttpHandler wrap(HttpHandler handler) {
return new LearningPushHandler(maxEntries, maxAge, handler);
return new LearningPushHandler(maxEntries, maxAge, maxPushEntries, maxPushAge, handler);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public PathHandler() {

public PathHandler(int cacheSize) {
if(cacheSize > 0) {
cache = new LRUCache<>(cacheSize, -1, true);
cache = new LRUCache<>(cacheSize, LRUCache.MAX_AGE_NO_EXPIRY, true);
} else {
cache = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import java.util.concurrent.ConcurrentHashMap;

import io.undertow.UndertowLogger;
import io.undertow.server.handlers.resource.CachingResourceManager;
import io.undertow.util.ConcurrentDirectDeque;
import org.xnio.BufferAllocator;

Expand All @@ -49,6 +48,14 @@
*/
public class DirectBufferCache {
private static final int SAMPLE_INTERVAL = 5;
/**
* Max age 0, indicating that entries expire upon creation and are not retained;
*/
public static final int MAX_AGE_NO_CACHING = 0;
/**
* Mage age -1, entries dont expire
*/
public static final int MAX_AGE_NO_EXPIRY = -1;

private final LimitedBufferSlicePool pool;
private final ConcurrentMap<Object, CacheEntry> cache;
Expand Down Expand Up @@ -98,12 +105,12 @@ public CacheEntry get(Object key) {
}

final long expires = cacheEntry.getExpires();
if(expires == CachingResourceManager.MAX_AGE_NO_CACHING || (expires > 0 && System.currentTimeMillis() > expires)) {
if(expires == MAX_AGE_NO_CACHING || (expires > 0 && System.currentTimeMillis() > expires)) {
remove(key);
return null;
}

//either did not expire or CachingResourceManager.MAX_AGE_NO_EXPIRY
//either did not expire or MAX_AGE_NO_EXPIRY
if (cacheEntry.hit() % SAMPLE_INTERVAL == 0) {

bumpAccess(cacheEntry);
Expand Down Expand Up @@ -235,18 +242,18 @@ public boolean enabled() {
}

public void enable() {
if(this.maxAge == CachingResourceManager.MAX_AGE_NO_CACHING) {
this.expires = CachingResourceManager.MAX_AGE_NO_CACHING;
if(this.maxAge == MAX_AGE_NO_CACHING) {
this.expires = MAX_AGE_NO_CACHING;
disable();
} else if(this.maxAge == CachingResourceManager.MAX_AGE_NO_EXPIRY) {
this.expires = CachingResourceManager.MAX_AGE_NO_EXPIRY;
} else if(this.maxAge == MAX_AGE_NO_EXPIRY) {
this.expires = MAX_AGE_NO_EXPIRY;
this.enabled = 2;
} else if(this.maxAge > 0) {
this.expires = System.currentTimeMillis() + maxAge;
this.enabled = 2;
} else {
this.expires = CachingResourceManager.MAX_AGE_NO_CACHING;
UndertowLogger.ROOT_LOGGER.wrongCacheTTLValue(this.maxAge, CachingResourceManager.MAX_AGE_NO_CACHING);
this.expires = MAX_AGE_NO_CACHING;
UndertowLogger.ROOT_LOGGER.wrongCacheTTLValue(this.maxAge, MAX_AGE_NO_CACHING);
disable();
}
}
Expand Down
23 changes: 20 additions & 3 deletions core/src/main/java/io/undertow/server/handlers/cache/LRUCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

package io.undertow.server.handlers.cache;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
Expand All @@ -40,6 +42,14 @@
*/
public class LRUCache<K, V> {
private static final int SAMPLE_INTERVAL = 5;
/**
* Max age 0, indicating that entries expire upon creation and are not retained;
*/
public static final int MAX_AGE_NO_CACHING = 0;
/**
* Mage age -1, entries dont expire
*/
public static final int MAX_AGE_NO_EXPIRY = -1;

/**
* Max active entries that are present in the cache.
Expand All @@ -61,6 +71,7 @@ public LRUCache(int maxEntries, final int maxAge) {
this.maxEntries = maxEntries;
this.fifo = false;
}

public LRUCache(int maxEntries, final int maxAge, boolean fifo) {
this.maxAge = maxAge;
this.cache = new ConcurrentHashMap<>(16);
Expand All @@ -73,8 +84,10 @@ public void add(K key, V newValue) {
CacheEntry<K, V> value = cache.get(key);
if (value == null) {
long expires;
if(maxAge == -1) {
expires = -1;
if(maxAge == MAX_AGE_NO_EXPIRY) {
expires = MAX_AGE_NO_EXPIRY;
} else if (maxAge == MAX_AGE_NO_CACHING) {
return;
} else {
expires = System.currentTimeMillis() + maxAge;
}
Expand All @@ -101,7 +114,7 @@ public V get(K key) {
return null;
}
long expires = cacheEntry.getExpires();
if(expires != -1) {
if(expires != MAX_AGE_NO_EXPIRY) {
if(System.currentTimeMillis() > expires) {
remove(key);
return null;
Expand All @@ -117,6 +130,10 @@ public V get(K key) {
return cacheEntry.getValue();
}

public Set<K> keySet(){
return Collections.unmodifiableSet(this.cache.keySet());
}

private void bumpAccess(CacheEntry<K, V> cacheEntry) {
Object prevToken = cacheEntry.claimToken();
if (!Boolean.FALSE.equals(prevToken)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ public class CachingResourceManager implements ResourceManager {
/**
* Max age 0, indicating that entries expire upon creation and are not retained;
*/
public static final int MAX_AGE_NO_CACHING = 0;
public static final int MAX_AGE_NO_CACHING = LRUCache.MAX_AGE_NO_CACHING;
/**
* Mage age -1, this force manager to retain entries until underlying resource manager indicate that entries expired/changed
*/
public static final int MAX_AGE_NO_EXPIRY = -1;
public static final int MAX_AGE_NO_EXPIRY = LRUCache.MAX_AGE_NO_EXPIRY;
/**
* The biggest file size we cache
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ public ServletPathMatches(final Deployment deployment) {
this.deployment = deployment;
this.welcomePages = deployment.getDeploymentInfo().getWelcomePages().toArray(new String[deployment.getDeploymentInfo().getWelcomePages().size()]);
this.resourceManager = deployment.getDeploymentInfo().getResourceManager();
this.pathMatchCacheFixed = new LRUCache<>(1000, -1, true);
this.pathMatchCacheFixed = new LRUCache<>(1000, LRUCache.MAX_AGE_NO_EXPIRY, true);
this.pathMatchCacheResources = new LRUCache<>(1000,
resourceManager instanceof CachingResourceManager? ((CachingResourceManager) resourceManager).getMaxAge() : -1, true);
resourceManager instanceof CachingResourceManager ? ((CachingResourceManager) resourceManager).getMaxAge() : CachingResourceManager.MAX_AGE_NO_EXPIRY, true);
// add change listener for welcome pages
if (this.resourceManager.isResourceChangeListenerSupported()) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public ServletContextImpl(final ServletContainer servletContainer, final Deploym
this.attributes = deploymentInfo.getServletContextAttributeBackingMap();
}
attributes.putAll(deployment.getDeploymentInfo().getServletContextAttributes());
this.contentTypeCache = new LRUCache<>(deployment.getDeploymentInfo().getContentTypeCacheSize(), -1, true);
this.contentTypeCache = new LRUCache<>(deployment.getDeploymentInfo().getContentTypeCacheSize(), LRUCache.MAX_AGE_NO_EXPIRY, true);
this.defaultSessionTimeout = deploymentInfo.getDefaultSessionTimeout() / 60;
}

Expand Down
Loading