Submitted on Jan 18th 2025 at 00:07:35 UTC by @gln for
Report ID: #38920
Report Type: Blockchain/DLT
Report severity: Medium
Target: https://github.com/Consensys/teku
Impacts:
Increasing greater than or equal to 25% of network processing node resource consumption by at least 30% without brute force actions, compared to the preceding 24 hours
Description
Brief/Intro
Teku does not validate parameters of incoming BlobSidecarsByRange requests.
Malformed BlobSidecarsByRange request allows an attacker to bypass validateRequest() method and also approveObjectsCount() (internal rate limiter).
Vulnerability Details
Let's look at the code which process req/resp requests https://github.com/Consensys/teku/blob/master/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/core/Eth2IncomingRequestHandler.java#L100
private void handleRequest(
final Optional<Eth2Peer> peer,
final TRequest request,
final ResponseCallback<TResponse> callback) {
try {
requestHandled.set(true);
final Optional<RpcException> requestValidationError =
1) localMessageHandler.validateRequest(protocolId, request);
if (requestValidationError.isPresent()) {
callback.completeWithErrorResponse(requestValidationError.get());
return;
}
2) localMessageHandler.onIncomingMessage(protocolId, peer, request, callback);
} catch (final StreamClosedException e) {
...
Corresponding validateRequest() method is called to verify the request
If validateRequest() do not return error, actual message handler is called
Let's look at BlobSidecarsByRange handler https://github.com/Consensys/teku/blob/master/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/BlobSidecarsByRangeMessageHandler.java#L85
public Optional<RpcException> validateRequest(
final String protocolId, final BlobSidecarsByRangeRequestMessage request) {
final SpecConfigDeneb specConfig =
SpecConfigDeneb.required(spec.atSlot(request.getMaxSlot()).getConfig());
final int maxRequestBlobSidecars = specConfig.getMaxRequestBlobSidecars();
final int maxBlobsPerBlock = specConfig.getMaxBlobsPerBlock();
1) final long requestedCount = calculateRequestedCount(request, maxBlobsPerBlock);
3) if (requestedCount > maxRequestBlobSidecars) {
requestCounter.labels("count_too_big").inc();
return Optional.of(
new RpcException(
INVALID_REQUEST_CODE,
String.format(
"Only a maximum of %s blob sidecars can be requested per request",
maxRequestBlobSidecars)));
}
return Optional.empty();
}
private long calculateRequestedCount(
final BlobSidecarsByRangeRequestMessage message, final int maxBlobsPerBlock) {
2) return maxBlobsPerBlock * message.getCount().longValue();
}
To verify request parameters calculateRequestedCount is called
Basically message.getCount() returns uint64, the type of return value is long, if message.getCount() is large enough, integer overflow will occur and result will be negative
This check will pass if requestedCount is negative
Thus validateRequest() checks can be bypassed.
Let's look at BlobSidecarsByRange handler, there are few more checks https://github.com/Consensys/teku/blob/master/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/BlobSidecarsByRangeMessageHandler.java#L110
public void onIncomingMessage(
final String protocolId,
final Eth2Peer peer,
final BlobSidecarsByRangeRequestMessage message,
final ResponseCallback<BlobSidecar> callback) {
final UInt64 startSlot = message.getStartSlot();
final UInt64 endSlot = message.getMaxSlot();
final SpecConfigDeneb specConfig = SpecConfigDeneb.required(spec.atSlot(endSlot).getConfig());
1) final long requestedCount = calculateRequestedCount(message, specConfig.getMaxBlobsPerBlock());
final Optional<RequestApproval> blobSidecarsRequestApproval =
2) peer.approveBlobSidecarsRequest(callback, requestedCount);
3) if (!peer.approveRequest() || blobSidecarsRequestApproval.isEmpty()) {
requestCounter.labels("rate_limited").inc();
return;
}
requestCounter.labels("ok").inc();
totalBlobSidecarsRequestedCounter.inc(message.getCount().longValue());
...
Assuming message is malformed, so calculateRequestedCount() will return negative value
We need to look at approveBlobSidecarsRequest() method https://github.com/Consensys/teku/blob/master/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DefaultEth2Peer.java#L386
public Optional<RequestApproval> approveBlobSidecarsRequest(
final ResponseCallback<BlobSidecar> callback, final long blobSidecarsCount) {
return approveObjectsRequest(
"blob sidecars", blobSidecarsRequestTracker, blobSidecarsCount, callback);
}
The approveObjectsRequest() method will finally call this method https://github.com/Consensys/teku/blob/master/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/RateTrackerImpl.java#L48
public synchronized Optional<RequestApproval> approveObjectsRequest(final long objectsCount) {
pruneRequests();
final UInt64 currentTime = timeProvider.getTimeInSeconds();
1) if (peerRateLimit - objectsWithinWindow <= 0) {
return Optional.empty();
}
2) objectsWithinWindow += objectsCount;
final RequestApproval requestApproval =
new RequestApproval.RequestApprovalBuilder()
.requestId(newRequestId++)
.timeSeconds(currentTime)
.objectsCount(objectsCount)
.build();
requests.put(requestApproval.getRequestKey(), objectsCount);
return Optional.of(requestApproval);
}
if objectsWithinWindow is negative, this check will not pass
here objectsWithinWindow can be made negative (if objectCount is negative)
As a result, negative objectsCount allows us to bypass internal rate limiter and request as many blobs as there are available.
Impact Details
Maximum amount of space occupied by blobs is around 50GB https://lighthouse-book.sigmaprime.io/advanced-blobs.html
This is a potential Denial of Service issue, as an attacker could request huge amount of blobs from a teku node.
XXXXKE requesting blobs for slots 1..1000
XXXXKE validation_result=Optional[tech.pegasys.teku.networking.eth2.rpc.core.RpcException: [Code 1] Only a maximum of 768 blob sidecars can be requested per request]
XXXKE requesting blobs for slots 1..(MAX_VALUE-100000)
XXXXKE validateRequest OK
XXXXKE validation_result=Optional.empty
XXXXKE sendBlobSidecars call...
when trying to request 1000 blob sidecars, validateRequest() will return error
when trying to request uint64.MAX_VALUE-1000000 sidecars, valideRequest() check will pass and sendBlobSidecars() will be called